├── .github └── workflows │ ├── coverage.yml │ └── readme.yml ├── .gitignore ├── .make ├── get-started.mk ├── help.mk ├── tag.mk └── test.mk ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gofield │ ├── calculate.go │ ├── creator.go │ ├── files.go │ ├── files_test.go │ ├── main.go │ ├── main_test.go │ ├── optimize.go │ ├── print.go │ ├── render.go │ ├── size.go │ ├── size_test.go │ ├── types.go │ ├── types_test.go │ ├── utils.go │ └── utils_test.go ├── example.png ├── example ├── filex.go ├── ignore.go ├── ignore │ ├── ignore.go │ ├── userx.go │ └── userx_test.go ├── userx.go └── userx_test.go ├── example_ignore.png ├── example_view.png ├── go.mod ├── go.sum ├── tests ├── enter │ └── file.go └── out │ └── file.go └── version.go /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '1.22' 19 | 20 | - name: Install goveralls 21 | run: go install github.com/mattn/goveralls@latest 22 | 23 | - name: Run tests 24 | run: make test 25 | 26 | - name: Send coverage 27 | env: 28 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 29 | run: goveralls -coverprofile=.temp/coverage-report.out -service=github 30 | -------------------------------------------------------------------------------- /.github/workflows/readme.yml: -------------------------------------------------------------------------------- 1 | name: update_version_on_tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | update-readme: 13 | if: "!contains(github.event.head_commit.message, '[automated] Update version')" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: '1.22' 24 | 25 | - name: Get the version 26 | id: get_version 27 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 28 | 29 | # README 30 | - name: Update README.md badges 31 | run: | 32 | current_time=$(date +%s) 33 | sed -i 's/\(ver=\)[0-9]*/\1'"$current_time"'/g' README.md 34 | 35 | # VERSION 36 | - name: Update version.go 37 | run: | 38 | sed -i 's/const Version = "[0-9.]*"/const Version = "${{ steps.get_version.outputs.VERSION }}"/' version.go 39 | git add version.go 40 | 41 | - name: Commit changes 42 | run: | 43 | git config --local user.email "action@github.com" 44 | git config --local user.name "GitHub Action" 45 | git add README.md version.go 46 | git commit -m "[automated] Update version to ${{ steps.get_version.outputs.VERSION }}" 47 | 48 | - name: Push changes 49 | run: | 50 | git push origin HEAD:${{ github.event.repository.default_branch }} 51 | 52 | - name: Move tag 53 | run: | 54 | git push --delete origin ${{ github.ref }} 55 | git tag -fa ${{ github.ref_name }} -m "[automated] Update version to ${{ steps.get_version.outputs.VERSION }}" 56 | git push origin ${{ github.ref_name }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .idea 3 | .temp 4 | .bin 5 | .env 6 | /dist 7 | /docs 8 | /gen 9 | /logs 10 | .vercel 11 | debug.log 12 | /.cache 13 | .PID 14 | 15 | .vscode 16 | 17 | node_modules/ 18 | /.gradle/ 19 | /coverage.out 20 | -------------------------------------------------------------------------------- /.make/get-started.mk: -------------------------------------------------------------------------------- 1 | ################################################################## Get Started 2 | download: 3 | go mod download 4 | 5 | tidy: 6 | go mod tidy 7 | 8 | -------------------------------------------------------------------------------- /.make/help.mk: -------------------------------------------------------------------------------- 1 | # Default target 2 | .DEFAULT_GOAL := help 3 | 4 | # Help 5 | help: 6 | @echo "Get Started:" 7 | @echo " download - Download Go module dependencies" 8 | @echo " tidy - Tidy Go module dependencies" 9 | @echo "" 10 | @echo "Tag:" 11 | @echo " tag - Show current git tag" 12 | @echo " tag-up - Update git tag" 13 | @echo "" 14 | @echo "Helps:" 15 | @echo " help - Show this help message" 16 | @echo "" 17 | @echo "APP:" 18 | @echo " build - Build the project" 19 | @echo " example - Run the project as an example" 20 | @echo " install - Install the application" 21 | @echo " test - Run tests" 22 | @echo "" 23 | @echo "Example:" 24 | @echo " make example" 25 | @echo " make install" 26 | 27 | 28 | 29 | # Phony targets 30 | .PHONY: help 31 | -------------------------------------------------------------------------------- /.make/tag.mk: -------------------------------------------------------------------------------- 1 | ################################################################## git-version 2 | GIT_LAST_TAG = @$(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -n 1 || echo "v0.0.0") 3 | GIT_NEW_TAG = $(shell echo $(GIT_LAST_TAG) | awk -F. '{$$NF = $$NF + 1;} 1' | sed 's/ /./g' | sed 's/^@v//') 4 | CURRENT_BRANCH = $(shell git rev-parse --abbrev-ref HEAD) 5 | 6 | tag: 7 | @git fetch --tags 8 | @echo $(GIT_LAST_TAG) | sed 's/^@v//' 9 | 10 | tag-new: 11 | @echo $(GIT_NEW_TAG) 12 | 13 | tag-up: 14 | @if [ "$(CURRENT_BRANCH)" != "main" ]; then \ 15 | echo "Error: You can only create new tags from the 'main' branch."; \ 16 | echo "Current branch: $(CURRENT_BRANCH)"; \ 17 | exit 1; \ 18 | fi 19 | @git fetch --tags --force 20 | @echo $(GIT_NEW_TAG) && git tag "v$(GIT_NEW_TAG)" && git push origin --force "v$(GIT_NEW_TAG)" 21 | -------------------------------------------------------------------------------- /.make/test.mk: -------------------------------------------------------------------------------- 1 | DEV_DIR := $(CURDIR) 2 | COVERAGE_FILE := $(DEV_DIR)/.temp/coverage-report.out 3 | HTML_COVERAGE := $(DEV_DIR)/.temp/coverage-report.html 4 | 5 | test: 6 | @mkdir -p $(DEV_DIR)/.temp 7 | @go clean -testcache 8 | @CGO_ENABLED=0 go test $(DEV_DIR)/cmd/gofield -coverprofile=coverage.tmp.out -covermode count -count 3 9 | @grep -v 'mocks\|config\|main\.go' coverage.tmp.out > $(COVERAGE_FILE) 10 | @rm coverage.tmp.out 11 | @go tool cover -html=$(COVERAGE_FILE) -o $(HTML_COVERAGE); 12 | @go tool cover -func=$(COVERAGE_FILE) | grep "total"; 13 | 14 | .PHONY: test 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2024, Vitalii Rozhkov 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DEV_DIR := $(CURDIR) 2 | APP_REPOSITORY := github.com/t34-dev 3 | APP_NAME := gofield 4 | APP_EXT := $(if $(filter Windows_NT,$(OS)),.exe) 5 | export GOPRIVATE=$(APP_REPOSITORY)/* 6 | 7 | # includes 8 | include .make/get-started.mk 9 | include .make/tag.mk 10 | include .make/test.mk 11 | include .make/help.mk 12 | 13 | build: 14 | @go build -o .bin/$(APP_NAME)$(APP_EXT) ./cmd/gofield 15 | 16 | example: build 17 | @.bin/$(APP_NAME)${APP_EXT} --files "example" -v 18 | 19 | install: build 20 | @cp .bin/$(APP_NAME)${APP_EXT} $(GOPATH)/bin/$(APP_NAME)$(APP_EXT) 21 | 22 | .PHONY: install build example 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-Field 2 | 3 | [![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) 4 | [![Coverage Status](https://coveralls.io/repos/github/t34-dev/go-field-alignment/badge.svg?branch=main&ver=1744642252)](https://coveralls.io/github/t34-dev/go-field-alignment?branch=main&ver=1744642252) 5 | ![Go Version](https://img.shields.io/badge/Go-1.22-blue?logo=go&ver=1744642252) 6 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/t34-dev/go-field-alignment?ver=1744642252) 7 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/t34-dev/go-field-alignment?sort=semver&style=flat&logo=git&logoColor=white&label=Latest%20Version&color=blue&ver=1744642252) 8 | 9 | Go-Field is a powerful tool designed for Golang developers to enhance code readability and optimize memory usage by performing multi-level field alignment in struct declarations while preserving original metadata. 10 | 11 | ![Go-Field Example](./example.png) 12 | ![Go-Field Example2](./example_view.png) 13 | ![Go-Field Example3](./example_ignore.png) 14 | 15 | ## Features 16 | 17 | - Analyzes struct field alignment and padding in Go source files 18 | - Calculates the size and alignment of each struct and its fields 19 | - Generics support 20 | - single-pass iteration 21 | - flexible configuration of files and directories for search 22 | - flexible configuration of ignore files (full control) 23 | - Optimizes struct layout by reordering fields for better memory efficiency 24 | - Performs multi-level struct field alignment for improved readability 25 | - Preserves original comments and metadata 26 | - Supports nested structs and complex type hierarchies 27 | - Processes single files or entire directories 28 | - Offers an option to automatically apply optimizations to source files 29 | - Easily integrates with existing Go projects 30 | - Provides debug mode for detailed analysis 31 | - Supports custom file pattern matching and ignoring 32 | 33 | ## Installation 34 | 35 | To install `gofield`, make sure you have Go installed on your system, then run: 36 | 37 | ```shell 38 | # get all versions 39 | go list -m -versions github.com/t34-dev/go-field-alignment/v2 40 | 41 | # get package 42 | go get -u github.com/t34-dev/go-field-alignment/v2@latest 43 | 44 | # install package 45 | go install github.com/t34-dev/go-field-alignment/v2/cmd/gofield@latest 46 | or 47 | go install github.com/t34-dev/go-field-alignment/v2/cmd/gofield@v2.0.3 48 | ``` 49 | 50 | For local installation: 51 | 52 | ```shell 53 | # Bash 54 | go build -o $GOPATH/bin/gofield # Unix 55 | go build -o $GOPATH/bin/gofield.exe # Windows 56 | 57 | # Makefile 58 | make install # Any system 59 | ``` 60 | 61 | ## Usage 62 | 63 | ``` 64 | gofield [options] 65 | ``` 66 | 67 | ### Options 68 | 69 | - `--files`, `-f`: Comma-separated list of files or folders to process (required) 70 | - `--ignore`, `-i`: Comma-separated list of files or folders to ignore 71 | - `--view`, `-v`: Print the absolute paths of found files 72 | - `--fix`: Make changes to the files 73 | - `--pattern`: Regex pattern for files to process (default: `\.go$`) 74 | - `--ignore-pattern`: Regex pattern for files to ignore 75 | - `--version`: Print the version of the program 76 | - `--help`: Print usage information 77 | - `--debug`: Enable debug mode 78 | 79 | ### Examples 80 | 81 | 1. Analyze all Go files in the current directory: 82 | ``` 83 | gofield --files . 84 | ``` 85 | 86 | 2. Optimize structs in specific files: 87 | ``` 88 | gofield --files main.go,utils.go --fix 89 | ``` 90 | 91 | 3. Process files matching a custom pattern (default: `\.go$`): 92 | ``` 93 | gofield --files src --pattern "\\.(go|proto)$" 94 | ``` 95 | 96 | 4. Ignore test files: 97 | ``` 98 | gofield --files . --ignore-pattern "_test\\.go$" 99 | ``` 100 | 101 | 5. View files that would be processed without making changes: 102 | ``` 103 | gofield --files src,pkg --view 104 | gofield --files "./internal/models/, ./cmd/" --view 105 | ``` 106 | 107 | 6. Process multiple directories while ignoring specific folders: 108 | ``` 109 | gofield --files "gofield,internal" --ignore "internal/generated" 110 | ``` 111 | 112 | 7. Use debug mode for detailed analysis: 113 | ``` 114 | gofield --files main.go --debug 115 | ``` 116 | 117 | 8. Combine multiple options: 118 | ``` 119 | gofield --files "src,pkg" --ignore "pkg/generated" --pattern "\\.(go|pb\\.go)$" --fix --view 120 | ``` 121 | 122 | 9. Ignore all test files + one special file.: 123 | ``` 124 | gofield -f . --ignore-pattern "file\.go|_test\.go$" 125 | ``` 126 | 127 | ## Output 128 | 129 | For each struct found in the processed files, `gofield` will output: 130 | 131 | - Struct name 132 | - Total size of the struct (before and after optimization) 133 | - Alignment of the struct 134 | - For each field: 135 | - Field name 136 | - Field type 137 | - Offset within the struct 138 | - Size of the field 139 | - Alignment of the field 140 | 141 | If the `--fix` option is used, it will also show the optimized layout of the struct and apply the changes to the source files. 142 | 143 | When using the `--debug` option, Go-Field provides a detailed before-and-after comparison of struct layouts. 144 | 145 | ## How It Works 146 | 147 | 1. Go-Field parses the specified Go source files and identifies all struct declarations. 148 | 2. It analyzes the current layout of each struct, calculating sizes, alignments, and paddings. 149 | 3. The tool then optimizes the struct layout by reordering fields to minimize padding while maintaining correct alignment. 150 | 4. If the `--fix` option is used, Go-Field rewrites the struct declarations in the source files with the optimized layout. 151 | 5. The tool preserves all comments and formatting to maintain code readability. 152 | 153 | ## Best Practices 154 | 155 | - Run Go-Field on your project before committing changes to ensure optimal struct layouts. 156 | - Use the `--view` option to preview which files would be affected before applying fixes. 157 | - Integrate Go-Field into your CI/CD pipeline to catch suboptimal struct layouts early. 158 | - When optimizing performance-critical code, use Go-Field in conjunction with benchmarking to measure the impact of struct optimizations. 159 | 160 | ## Contributing 161 | 162 | Contributions are welcome! Please feel free to submit a Pull Request. Here are some ways you can contribute: 163 | 164 | - Improve documentation 165 | - Add new features 166 | - Fix bugs 167 | - Optimize performance 168 | - Write tests 169 | 170 | Before submitting a pull request, please ensure your code passes all tests and adheres to the project's coding standards. 171 | 172 | ## License 173 | 174 | This project is licensed under the ISC License. See the [LICENSE](LICENSE) file for details. 175 | 176 | ## Support 177 | 178 | If you encounter any issues or have questions, please file an issue on the [GitHub repository](https://github.com/t34-dev/go-field-alignment/issues). 179 | 180 | --- 181 | 182 | Developed with ❤️ by [T34](https://github.com/t34-dev) 183 | -------------------------------------------------------------------------------- /cmd/gofield/calculate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // calculateStructures calculates the size and alignment of structures in the given slice of Structure. 4 | // It recursively processes nested structures and updates their size and alignment information. 5 | func calculateStructure(elem *Structure, cache map[string]*Structure) { 6 | var currentOffset, maxAlign uintptr 7 | for _, field := range elem.NestedFields { 8 | var fieldSize, fieldAlign uintptr 9 | 10 | isValidCustomType := isValidCustomTypeName(field.StringType) 11 | 12 | if field.IsStructure { 13 | calculateStructure(field, cache) 14 | } 15 | 16 | if item, ok := cache[field.StringType]; ok { 17 | fieldSize = item.Size 18 | fieldAlign = item.Align 19 | } else if item, ok = cache[elem.Path]; ok { 20 | fieldSize = item.Size 21 | fieldAlign = item.Align 22 | } else { 23 | if field.IsStructure { 24 | fieldSize, fieldAlign = calculateStructLayout(field) 25 | } else { 26 | fieldSize = getFieldSize(field.StructType) 27 | fieldAlign = getFieldAlign(field.StructType) 28 | } 29 | } 30 | 31 | currentOffset = align(currentOffset, fieldAlign) 32 | 33 | field.Size = fieldSize 34 | field.Align = fieldAlign 35 | field.Offset = currentOffset 36 | 37 | if isValidCustomType { 38 | cache[field.StringType] = field 39 | } else { 40 | cache[field.Path] = field 41 | } 42 | 43 | currentOffset += fieldSize 44 | 45 | if fieldAlign > maxAlign { 46 | maxAlign = fieldAlign 47 | } 48 | } 49 | 50 | elem.Size = align(currentOffset, maxAlign) 51 | elem.Align = maxAlign 52 | } 53 | 54 | func calculateStructLayout(field *Structure) (size, alignment uintptr) { 55 | var offset uintptr = 0 56 | maxAlign := uintptr(1) 57 | 58 | for _, field := range field.NestedFields { 59 | offset = align(offset, field.Align) 60 | if field.Align > maxAlign { 61 | maxAlign = field.Align 62 | } 63 | offset += field.Size 64 | } 65 | size = align(offset, maxAlign) 66 | alignment = maxAlign 67 | 68 | return size, alignment 69 | } 70 | 71 | // calculateStructure calculates the size and alignment of a single structure. 72 | // It updates the Size and Align fields of the Structure and processes nested fields. 73 | func calculateStructures(structures []*Structure, isBefore bool) { 74 | cache := make(map[string]*Structure, len(structures)) 75 | for _, structure := range structures { 76 | calculateStructure(structure, cache) 77 | if isBefore { 78 | structure.MetaData.BeforeSize = structure.Size 79 | } else { 80 | structure.MetaData.AfterSize = structure.Size 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/gofield/creator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | ) 6 | 7 | // ============= Creator Item 8 | 9 | // createTypeName extracts the name from a type spec. 10 | func createTypeName(spec *ast.TypeSpec) string { 11 | return spec.Name.Name 12 | } 13 | 14 | // createFieldNames extracts field names from a field spec. 15 | // 16 | // Returns multiple names when the specified field spec is actually a definition of multiple same-typed fields. 17 | // Returns a slice with a single empty string in case of embedded types. 18 | // 19 | // type A1 struct{} 20 | // 21 | // type A2 struct { 22 | // A1 // <-------- []string{""} 23 | // F1 int // <-------- []string{"F1"} 24 | // F2 string // <-------- []string{"F2"} 25 | // F3, F4, F5 string // <-------- []string{"F3", "F4", "F5"} 26 | // } 27 | func createFieldNames(field *ast.Field) []string { 28 | c := len(field.Names) 29 | if c == 0 { 30 | // This is a case of an embedded type - return a slice with an empty string 31 | return []string{""} 32 | } 33 | names := make([]string, c) 34 | for i := 0; i < c; i++ { 35 | names[i] = field.Names[i].Name 36 | } 37 | return names 38 | } 39 | 40 | // createItemInfoPath generates a path string for an item. 41 | // It combines the item's name with its parent's name, if available. 42 | func createItemInfoPath(name, parentName string) string { 43 | if parentName != "" { 44 | return parentName + "/" + name 45 | } 46 | return name 47 | } 48 | 49 | // createTypeItemInfo creates a Structure from the given AST type node. 50 | // It processes its contents and creates nested structures as needed. 51 | // The function also updates the provided mapper with the created Structure. 52 | func createTypeItemInfo(typeSpec *ast.TypeSpec, parent *Structure, mapper map[string]*Structure) *Structure { 53 | typeInfo := &Structure{ 54 | Name: createTypeName(typeSpec), 55 | Root: typeSpec, 56 | StructType: typeSpec.Type, 57 | IsStructure: true, 58 | StringType: getTypeString(typeSpec.Type), 59 | } 60 | if typeInfo.Name == "" { 61 | typeInfo.Name = "!" + typeInfo.StringType 62 | } 63 | 64 | typeInfo.Path = createItemInfoPath(typeInfo.Name, "") 65 | mapper[typeInfo.Path] = typeInfo 66 | 67 | if structType, ok := typeSpec.Type.(*ast.StructType); ok { 68 | for _, field := range structType.Fields.List { 69 | newFields := createFieldItemsInfo(field, typeInfo, mapper) 70 | if len(newFields) > 0 { 71 | typeInfo.NestedFields = append(typeInfo.NestedFields, newFields...) 72 | } 73 | } 74 | } 75 | 76 | return typeInfo 77 | } 78 | 79 | // createFieldItemsInfo creates a list of Structure-s from the given AST field node. 80 | // The number of returned Structure-s is defined by how field node looks like. 81 | // It processes its contents and creates nested structures as needed. 82 | // The function also updates the provided mapper with the created Structures. 83 | func createFieldItemsInfo(field *ast.Field, parent *Structure, mapper map[string]*Structure) []*Structure { 84 | fieldNames := createFieldNames(field) 85 | fieldInfos := make([]*Structure, len(fieldNames)) 86 | for i, name := range fieldNames { 87 | fieldInfos[i] = createSingleFieldItemInfo(name, field, parent, mapper) 88 | } 89 | return fieldInfos 90 | } 91 | 92 | // createSingleFieldItemInfo creates a single Structure from the given AST field node. 93 | // It processes its contents and creates nested structures as needed. 94 | // The function also updates the provided mapper with the created Structure. 95 | func createSingleFieldItemInfo(name string, field *ast.Field, parent *Structure, mapper map[string]*Structure) *Structure { 96 | fieldInfo := &Structure{ 97 | Name: name, 98 | RootField: field, 99 | StructType: field.Type, 100 | StringType: getTypeString(field.Type), 101 | } 102 | if fieldInfo.Name == "" { 103 | fieldInfo.Name = "!" + fieldInfo.StringType 104 | } 105 | 106 | fieldInfo.Path = createItemInfoPath(fieldInfo.Name, parent.Path) 107 | mapper[fieldInfo.Path] = fieldInfo 108 | 109 | switch typed := field.Type.(type) { 110 | case *ast.Ident: 111 | fieldInfo.IsStructure = typed.Obj != nil 112 | case *ast.StructType: 113 | fieldInfo.IsStructure = true 114 | for _, nestedField := range typed.Fields.List { 115 | nestedFieldItems := createFieldItemsInfo(nestedField, fieldInfo, mapper) 116 | if len(nestedFieldItems) > 0 { 117 | fieldInfo.NestedFields = append(fieldInfo.NestedFields, nestedFieldItems...) 118 | } 119 | } 120 | } 121 | 122 | return fieldInfo 123 | } 124 | -------------------------------------------------------------------------------- /cmd/gofield/files.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // mergeFlags combines the long and short form flags, prioritizing the long form if present. 14 | func mergeFlags(long, short string) []string { 15 | if long != "" { 16 | return splitAndTrim(long) 17 | } 18 | return splitAndTrim(short) 19 | } 20 | 21 | // splitAndTrim splits a string by commas and trims whitespace from each part. 22 | func splitAndTrim(s string) []string { 23 | parts := strings.Split(s, ",") 24 | var result []string 25 | for _, part := range parts { 26 | trimmed := strings.TrimSpace(part) 27 | if trimmed != "" { 28 | result = append(result, trimmed) 29 | } 30 | } 31 | return result 32 | } 33 | 34 | // findFiles searches for files matching the given regex patterns and ignoring specified files. 35 | func findFiles(files []string, fileRegex, ignoreRegex *regexp.Regexp, ignoreFiles map[string]interface{}) (map[string]interface{}, error) { 36 | filesMap := make(map[string]interface{}) 37 | 38 | for _, file := range files { 39 | absPath, _ := filepath.Abs(file) 40 | if _, err := os.Stat(absPath); os.IsNotExist(err) { 41 | return nil, fmt.Errorf("path does not exist: %s", absPath) 42 | } 43 | if err := findMatchingFiles(absPath, fileRegex, ignoreRegex, filesMap, ignoreFiles); err != nil { 44 | return nil, fmt.Errorf("error processing path %s: %v", absPath, err) 45 | } 46 | } 47 | return filesMap, nil 48 | } 49 | 50 | // findMatchingFiles walks through the directory structure and finds files matching the given patterns. 51 | func findMatchingFiles(path string, fileRegex, ignoreRegex *regexp.Regexp, oldFiles, ignoreFiles map[string]interface{}) error { 52 | return filepath.Walk(path, func(file string, info os.FileInfo, err error) error { 53 | if err != nil { 54 | if os.IsPermission(err) { 55 | fmt.Printf("Warning: Permission denied accessing %s\n", file) 56 | return nil 57 | } 58 | return fmt.Errorf("error accessing %s: %v", file, err) 59 | } 60 | 61 | if !info.IsDir() { 62 | basename := filepath.Base(file) 63 | if _, ok := ignoreFiles[file]; !ok { 64 | if fileRegex.MatchString(basename) && (ignoreRegex == nil || !ignoreRegex.MatchString(basename)) && oldFiles != nil { 65 | oldFiles[file] = struct{}{} 66 | } 67 | } 68 | } 69 | 70 | return nil 71 | }) 72 | } 73 | 74 | // fileProcessingOptions is a set of options which define how file gets processed. 75 | type fileProcessingOptions struct { 76 | viewMode bool 77 | fixMode bool 78 | debugMode bool 79 | } 80 | 81 | // processFile processes a file located at the specified path. 82 | // 83 | // Returns true if the file can be optimized (needs fix), false otherwise. 84 | func processFile(path string, opts fileProcessingOptions) (needFix bool, err error) { 85 | fileData, err := os.ReadFile(path) 86 | if err != nil { 87 | return false, fmt.Errorf("cannot read file %s: %w", path, err) 88 | } 89 | structures, mapStructures, err := ParseWithFilename(path, fileData) 90 | if err != nil { 91 | return false, fmt.Errorf("cannot parse file %s: %w", path, err) 92 | } 93 | 94 | calculateStructures(structures, true) 95 | 96 | oldStructures := make([]*Structure, 0, len(structures)) 97 | for _, structure := range structures { 98 | copied := deepCopy(structure) 99 | oldStructures = append(oldStructures, copied) 100 | } 101 | oldStructuresMapper := createMapper(oldStructures) 102 | 103 | optimizeMapperStructures(mapStructures) 104 | calculateStructures(structures, false) 105 | 106 | for _, structure := range structures { 107 | if structure.MetaData.BeforeSize > structure.MetaData.AfterSize { 108 | needFix = true 109 | break 110 | } 111 | } 112 | if opts.viewMode || needFix { 113 | fmt.Printf("%s\n", path) 114 | } 115 | for idx, structure := range structures { 116 | if structure.MetaData.BeforeSize > structure.MetaData.AfterSize { 117 | alert := fmt.Sprintf("can free %d bytes", structure.MetaData.BeforeSize-structure.MetaData.AfterSize) 118 | if opts.fixMode { 119 | alert = "Fixed" 120 | } 121 | fmt.Printf( 122 | "%s%-15s %d(b) -> %d(b) %s!\n", 123 | strings.Repeat(" ", 3), 124 | structure.Name, 125 | structure.MetaData.BeforeSize, 126 | structure.MetaData.AfterSize, 127 | alert, 128 | ) 129 | if opts.debugMode { 130 | oldStructure, ok := oldStructuresMapper[structure.Path] 131 | if ok { 132 | fmt.Printf("%s%-20s\n", strings.Repeat(" ", 9), "------------------------------------------ [BEFORE]") 133 | testPrintStructure(oldStructure, 9) 134 | fmt.Printf("%s%-20s\n", strings.Repeat(" ", 9), "------------------------------------------ [AFTER]") 135 | testPrintStructure(structure, 9) 136 | } 137 | } 138 | if idx != len(structures)-1 && opts.debugMode { 139 | fmt.Println() 140 | } 141 | } else { 142 | if opts.viewMode { 143 | fmt.Printf("%s%-15s ✓\n", strings.Repeat(" ", 3), structure.Name) 144 | } 145 | } 146 | } 147 | if opts.viewMode && len(structures) > 0 { 148 | fmt.Println() 149 | } 150 | 151 | if !opts.fixMode || !needFix { 152 | // If "fix" has not been requested or there's nothing to fix, exit 153 | return needFix, nil 154 | } 155 | 156 | // FIX 157 | renderTextStructures(structures) 158 | 159 | // Apply replacements 160 | resultData, err := Replacer(fileData, structures) 161 | if err != nil { 162 | return needFix, fmt.Errorf("cannot replace content in file: %w", err) 163 | } 164 | 165 | // Format results. 166 | // 167 | // They need to be formatted after all replacements have been applied 168 | formatted, err := format.Source(resultData) 169 | if err != nil { 170 | return needFix, fmt.Errorf("cannot format result content: %w", err) 171 | } 172 | 173 | // Write results 174 | err = os.WriteFile(path, formatted, 0644) 175 | if err != nil { 176 | return needFix, fmt.Errorf("cannot write results to file: %w", err) 177 | } 178 | return needFix, nil 179 | } 180 | 181 | // normalizeLineEndings converts all line endings to LF 182 | func normalizeLineEndings(data []byte) []byte { 183 | return bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) 184 | } 185 | -------------------------------------------------------------------------------- /cmd/gofield/files_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "reflect" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | // TestGoPadding is the main test function that runs all subtests for the gofield program. 15 | func TestGoPadding(t *testing.T) { 16 | // Setup: Create a temporary directory for test files 17 | tempDir, err := os.MkdirTemp("", "gofield-test") 18 | if err != nil { 19 | t.Fatalf("Failed to create temp dir: %v", err) 20 | } 21 | defer os.RemoveAll(tempDir) 22 | 23 | // Create test files 24 | createTestFiles(t, tempDir) 25 | 26 | // Run tests 27 | t.Run("BasicUsage", func(t *testing.T) { testBasicUsage(t, tempDir) }) 28 | t.Run("IgnoreFiles", func(t *testing.T) { testIgnoreFiles(t, tempDir) }) 29 | t.Run("ViewFiles", func(t *testing.T) { testViewFiles(t, tempDir) }) 30 | t.Run("ApplyFixes", func(t *testing.T) { testApplyFixes(t, tempDir) }) 31 | t.Run("FilePatterns", func(t *testing.T) { testFilePatterns(t, tempDir) }) 32 | t.Run("ErrorHandling", func(t *testing.T) { testErrorHandling(t, tempDir) }) 33 | t.Run("UtilityFunctions", func(t *testing.T) { testUtilityFunctions(t) }) 34 | t.Run("FlagCombinations", func(t *testing.T) { testFlagCombinations(t, tempDir) }) 35 | t.Run("PathsWithSpaces", func(t *testing.T) { testPathsWithSpaces(t, tempDir) }) 36 | t.Run("RecursiveTraversal", func(t *testing.T) { testRecursiveTraversal(t, tempDir) }) 37 | // Temporarily disable SymbolicLinks test on Windows 38 | if runtime.GOOS != "windows" { 39 | t.Run("SymbolicLinks", func(t *testing.T) { testSymbolicLinks(t, tempDir) }) 40 | } 41 | t.Run("AccessRights", func(t *testing.T) { testAccessRights(t, tempDir) }) 42 | } 43 | 44 | // createTestFiles creates a set of test files in the specified directory. 45 | func createTestFiles(t *testing.T, dir string) { 46 | files := []string{"main.go", "utils.go", "test.go", "ignore_me.go"} 47 | for _, file := range files { 48 | err := os.WriteFile(filepath.Join(dir, file), []byte("package main"), 0644) 49 | if err != nil { 50 | t.Fatalf("Failed to create test file %s: %v", file, err) 51 | } 52 | } 53 | 54 | // Create a subdirectory 55 | subDir := filepath.Join(dir, "subdir") 56 | if err := os.Mkdir(subDir, 0755); err != nil { 57 | t.Fatalf("Failed to create subdirectory: %v", err) 58 | } 59 | err := os.WriteFile(filepath.Join(subDir, "subfile.go"), []byte("package sub"), 0644) 60 | if err != nil { 61 | t.Fatalf("Failed to create test file in subdirectory: %v", err) 62 | } 63 | } 64 | 65 | // runCommand executes the gofield program with the given arguments and returns the output. 66 | func runCommand(args ...string) (string, error) { 67 | cmd := exec.Command("go", append([]string{"run", "."}, args...)...) 68 | output, err := cmd.CombinedOutput() 69 | return string(output), err 70 | } 71 | 72 | // testBasicUsage tests the basic usage of the gofield program. 73 | func testBasicUsage(t *testing.T, dir string) { 74 | output, err := runCommand("--files", filepath.Join(dir, "main.go")) 75 | if err != nil { 76 | t.Errorf("Basic usage failed: %v", err) 77 | } 78 | if !strings.Contains(output, "Files analyzed: 1") { 79 | t.Errorf("Unexpected output for basic usage: %s", output) 80 | } 81 | 82 | output, err = runCommand("-f", fmt.Sprintf("%s,%s", filepath.Join(dir, "main.go"), filepath.Join(dir, "utils.go"))) 83 | if err != nil { 84 | t.Errorf("Basic usage with short flag failed: %v", err) 85 | } 86 | if !strings.Contains(output, "Files analyzed: 2") { 87 | t.Errorf("Unexpected output for basic usage with short flag: %s", output) 88 | } 89 | } 90 | 91 | // testIgnoreFiles tests the file ignoring functionality of gofield. 92 | func testIgnoreFiles(t *testing.T, dir string) { 93 | output, err := runCommand("--files", dir, "--ignore", filepath.Join(dir, "test.go")+","+filepath.Join(dir, "ignore_me.go")) 94 | if err != nil { 95 | t.Errorf("Ignore files failed: %v", err) 96 | } 97 | if !strings.Contains(output, "Files analyzed: 3") { 98 | t.Errorf("Unexpected output for ignore files: %s", output) 99 | } 100 | 101 | output, err = runCommand("-f", dir, "-i", fmt.Sprintf("%s,%s", filepath.Join(dir, "test.go"), filepath.Join(dir, "ignore_me.go"))) 102 | if err != nil { 103 | t.Errorf("Ignore files with short flag failed: %v", err) 104 | } 105 | if !strings.Contains(output, "Files analyzed: 3") { 106 | t.Errorf("Unexpected output for ignore files with short flag: %s", output) 107 | } 108 | } 109 | 110 | // testViewFiles tests the file viewing functionality of gofield. 111 | func testViewFiles(t *testing.T, dir string) { 112 | output, err := runCommand("--files", dir, "--view") 113 | if err != nil { 114 | t.Errorf("View files failed: %v", err) 115 | } 116 | if !strings.Contains(output, filepath.Join(dir, "main.go")) { 117 | t.Errorf("View files did not show expected file: %s", output) 118 | } 119 | 120 | output, err = runCommand("-f", dir, "-v") 121 | if err != nil { 122 | t.Errorf("View files with short flag failed: %v", err) 123 | } 124 | if !strings.Contains(output, filepath.Join(dir, "utils.go")) { 125 | t.Errorf("View files with short flag did not show expected file: %s", output) 126 | } 127 | } 128 | 129 | // testApplyFixes tests the fix applying functionality of gofield. 130 | func testApplyFixes(t *testing.T, dir string) { 131 | filePath := filepath.Join(dir, "main.go") 132 | 133 | // Сохраняем исходное содержимое файла 134 | originalContent, err := os.ReadFile(filePath) 135 | if err != nil { 136 | t.Fatalf("Failed to read original file content: %v", err) 137 | } 138 | // Функция для восстановления исходного содержимого 139 | defer func() { 140 | err := os.WriteFile(filePath, originalContent, 0644) 141 | if err != nil { 142 | t.Errorf("Failed to restore original file content: %v", err) 143 | } 144 | }() 145 | 146 | // Добавляем тестовую структуру с невыровненными полями 147 | testStructure := `package main 148 | 149 | type TestStruct struct { 150 | a bool 151 | b int64 152 | c bool 153 | } 154 | ` 155 | err = os.WriteFile(filePath, []byte(testStructure), 0644) 156 | if err != nil { 157 | t.Fatalf("Failed to write test structure to file: %v", err) 158 | } 159 | 160 | output, err := runCommand("--files", filePath, "--fix") 161 | if err != nil { 162 | t.Errorf("Apply fixes failed: %v", err) 163 | } 164 | if !strings.Contains(output, "Applied fixes to 1 files") { 165 | t.Errorf("Unexpected output for apply fixes: %s", output) 166 | } 167 | // Проверяем, что структура была оптимизирована 168 | optimizedContent, err := os.ReadFile(filePath) 169 | if err != nil { 170 | t.Fatalf("Failed to read optimized file content: %v", err) 171 | } 172 | 173 | expectedOptimizedStructure := `package main 174 | 175 | type TestStruct struct { 176 | b int64 177 | a bool 178 | c bool 179 | }` 180 | if !strings.Contains(string(optimizedContent), expectedOptimizedStructure) { 181 | t.Errorf("Structure was not optimized as expected. Got:\n%s", string(optimizedContent)) 182 | } 183 | } 184 | 185 | // testFilePatterns tests the file pattern matching functionality of gofield. 186 | func testFilePatterns(t *testing.T, dir string) { 187 | output, err := runCommand("--files", dir, "--pattern", "\\.go$") 188 | if err != nil { 189 | t.Errorf("File patterns failed: %v", err) 190 | } 191 | if !strings.Contains(output, "Files analyzed: 5") { 192 | t.Errorf("Unexpected output for file patterns: %s", output) 193 | } 194 | 195 | output, err = runCommand("--files", dir, "--ignore-pattern", "test\\.go$") 196 | if err != nil { 197 | t.Errorf("Ignore patterns failed: %v", err) 198 | } 199 | if !strings.Contains(output, "Files analyzed: 4") { 200 | t.Errorf("Unexpected output for ignore patterns: %s", output) 201 | } 202 | } 203 | 204 | // testErrorHandling tests various error scenarios in gofield. 205 | func testErrorHandling(t *testing.T, dir string) { 206 | output, err := runCommand() 207 | if err != nil { 208 | t.Errorf("Unexpected error when running without arguments: %v", err) 209 | } 210 | if !strings.Contains(output, "Usage of gofield:") { 211 | t.Errorf("Expected usage information, got: %s", output) 212 | } 213 | 214 | output, err = runCommand("--files", "non_existent_folder") 215 | if err == nil { 216 | t.Errorf("Expected error for non-existent folder, got none. Output: %s", output) 217 | } 218 | if !strings.Contains(output, "path does not exist") { 219 | t.Errorf("Expected 'path does not exist' error, got: %s", output) 220 | } 221 | 222 | output, err = runCommand("--files", dir, "--pattern", "[") 223 | if err == nil { 224 | t.Errorf("Expected error for invalid regex, got none. Output: %s", output) 225 | } 226 | outputLower := strings.ToLower(output) 227 | if !strings.Contains(outputLower, "error compiling file pattern regex") { 228 | t.Errorf("Expected regex compilation error, got: %s", output) 229 | } 230 | if !strings.Contains(outputLower, "error parsing regexp: missing closing ]") { 231 | t.Errorf("Expected specific regex parsing error, got: %s", output) 232 | } 233 | } 234 | 235 | // testUtilityFunctions tests utility functions like version and help in gofield. 236 | func testUtilityFunctions(t *testing.T) { 237 | output, err := runCommand("--version") 238 | if err != nil { 239 | t.Errorf("Version check failed: %v", err) 240 | } 241 | if !strings.Contains(output, "Version: ") { 242 | t.Errorf("Unexpected output for version check: %s", output) 243 | } 244 | 245 | output, err = runCommand("--help") 246 | if err != nil { 247 | t.Errorf("Help check failed: %v", err) 248 | } 249 | if !strings.Contains(output, "Usage of gofield:") { 250 | t.Errorf("Unexpected output for help check: %s", output) 251 | } 252 | } 253 | 254 | // testFlagCombinations tests various combinations of flags in gofield. 255 | func testFlagCombinations(t *testing.T, dir string) { 256 | output, err := runCommand("--files", dir, "--ignore", filepath.Join(dir, "test.go"), "--view", "--pattern", "\\.go$") 257 | if err != nil { 258 | t.Errorf("Flag combinations failed: %v", err) 259 | } 260 | if !strings.Contains(output, "Files analyzed: 4") || !strings.Contains(output, filepath.Join(dir, "main.go")) { 261 | t.Errorf("Unexpected output for flag combinations: %s", output) 262 | } 263 | } 264 | 265 | // testPathsWithSpaces tests gofield's behavior with paths containing spaces. 266 | func testPathsWithSpaces(t *testing.T, dir string) { 267 | spaceDir := filepath.Join(dir, "folder with spaces") 268 | err := os.Mkdir(spaceDir, 0755) 269 | if err != nil { 270 | t.Fatalf("Failed to create directory with spaces: %v", err) 271 | } 272 | err = os.WriteFile(filepath.Join(spaceDir, "file.go"), []byte("package space"), 0644) 273 | if err != nil { 274 | t.Fatalf("Failed to create test file in directory with spaces: %v", err) 275 | } 276 | 277 | output, err := runCommand("--files", spaceDir) 278 | if err != nil { 279 | t.Errorf("Paths with spaces failed: %v", err) 280 | } 281 | if !strings.Contains(output, "Files analyzed: 1") { 282 | t.Errorf("Unexpected output for paths with spaces: %s", output) 283 | } 284 | } 285 | 286 | // testRecursiveTraversal tests gofield's recursive directory traversal. 287 | func testRecursiveTraversal(t *testing.T, dir string) { 288 | output, err := runCommand("--files", dir, "--pattern", "\\.go$") 289 | if err != nil { 290 | t.Errorf("Recursive traversal failed: %v", err) 291 | } 292 | if !strings.Contains(output, "Files analyzed: 6") { 293 | t.Errorf("Unexpected output for recursive traversal: %s", output) 294 | } 295 | } 296 | 297 | // testSymbolicLinks tests gofield's handling of symbolic links. 298 | func testSymbolicLinks(t *testing.T, dir string) { 299 | linkFile := filepath.Join(dir, "link_file.go") 300 | err := os.Symlink(filepath.Join(dir, "main.go"), linkFile) 301 | if err != nil { 302 | t.Fatalf("Failed to create symbolic link: %v", err) 303 | } 304 | 305 | output, err := runCommand("--files", linkFile) 306 | if err != nil { 307 | t.Errorf("Symbolic links failed: %v", err) 308 | } 309 | if !strings.Contains(output, "Files analyzed: 1") { 310 | t.Errorf("Unexpected output for symbolic links: %s", output) 311 | } 312 | } 313 | 314 | // testAccessRights tests gofield's behavior with files having different access rights. 315 | func testAccessRights(t *testing.T, dir string) { 316 | noAccessFile := filepath.Join(dir, "no_access.go") 317 | err := os.WriteFile(noAccessFile, []byte("package noaccess"), 0644) 318 | if err != nil { 319 | t.Fatalf("Failed to create no-access file: %v", err) 320 | } 321 | 322 | initialFileCount := countGoFiles(t, dir) 323 | 324 | output, err := runCommand("--files", dir) 325 | if err != nil { 326 | t.Errorf("Access rights test failed: %v", err) 327 | } 328 | 329 | minExpectedFiles := initialFileCount 330 | maxExpectedFiles := initialFileCount + 1 331 | 332 | actualFileCount := extractFileCount(output) 333 | if actualFileCount < minExpectedFiles || actualFileCount > maxExpectedFiles { 334 | t.Errorf("Expected to find between %d and %d files, but got: %d. Full output: %s", 335 | minExpectedFiles, maxExpectedFiles, actualFileCount, output) 336 | } 337 | 338 | if runtime.GOOS != "windows" { 339 | err = os.Chmod(noAccessFile, 0000) 340 | if err != nil { 341 | t.Fatalf("Failed to change file permissions: %v", err) 342 | } 343 | output, _ = runCommand("--files", dir) 344 | if !strings.Contains(output, "permission denied") { 345 | t.Errorf("Expected warning about permission denied on non-Windows systems, got: %s", output) 346 | } 347 | } 348 | } 349 | 350 | // countGoFiles counts the number of Go files in a directory. 351 | func countGoFiles(t *testing.T, dir string) int { 352 | count := 0 353 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 354 | if err != nil { 355 | return err 356 | } 357 | if !info.IsDir() && strings.HasSuffix(info.Name(), ".go") { 358 | count++ 359 | } 360 | return nil 361 | }) 362 | if err != nil { 363 | t.Fatalf("Failed to count .go files: %v", err) 364 | } 365 | return count 366 | } 367 | 368 | // extractFileCount extracts the number of files found from gofield's output. 369 | func extractFileCount(output string) int { 370 | for _, line := range strings.Split(output, "\n") { 371 | if strings.Contains(line, "Files analyzed:") { 372 | var count int 373 | _, err := fmt.Sscanf(line, "Files analyzed: %d", &count) 374 | if err == nil { 375 | return count 376 | } 377 | } 378 | } 379 | return -1 // Return -1 if unable to extract the number of files 380 | } 381 | 382 | func TestMergeFlags(t *testing.T) { 383 | tests := []struct { 384 | name string 385 | long string 386 | short string 387 | expected []string 388 | }{ 389 | { 390 | name: "Long flag present", 391 | long: "file1.go,file2.go", 392 | short: "short.go", 393 | expected: []string{"file1.go", "file2.go"}, 394 | }, 395 | { 396 | name: "Only short flag present", 397 | long: "", 398 | short: "short1.go,short2.go", 399 | expected: []string{"short1.go", "short2.go"}, 400 | }, 401 | { 402 | name: "Both flags empty", 403 | long: "", 404 | short: "", 405 | expected: nil, // Changed from []string{} to nil 406 | }, 407 | { 408 | name: "Long flag with spaces", 409 | long: " file1.go , file2.go ", 410 | short: "short.go", 411 | expected: []string{"file1.go", "file2.go"}, 412 | }, 413 | { 414 | name: "Short flag with empty parts", 415 | long: "", 416 | short: "short1.go,,short2.go", 417 | expected: []string{"short1.go", "short2.go"}, 418 | }, 419 | } 420 | 421 | for _, tt := range tests { 422 | t.Run(tt.name, func(t *testing.T) { 423 | result := mergeFlags(tt.long, tt.short) 424 | if !reflect.DeepEqual(result, tt.expected) { 425 | t.Errorf("mergeFlags(%q, %q) = %v; want %v", tt.long, tt.short, result, tt.expected) 426 | } 427 | }) 428 | } 429 | } 430 | func TestSplitAndTrim(t *testing.T) { 431 | tests := []struct { 432 | name string 433 | input string 434 | expected []string 435 | }{ 436 | { 437 | name: "Simple split", 438 | input: "a,b,c", 439 | expected: []string{"a", "b", "c"}, 440 | }, 441 | { 442 | name: "Split with spaces", 443 | input: " a , b , c ", 444 | expected: []string{"a", "b", "c"}, 445 | }, 446 | { 447 | name: "Split with empty parts", 448 | input: "a,,b,c,", 449 | expected: []string{"a", "b", "c"}, 450 | }, 451 | { 452 | name: "Single item", 453 | input: "a", 454 | expected: []string{"a"}, 455 | }, 456 | { 457 | name: "Empty string", 458 | input: "", 459 | expected: nil, // Changed from []string{} to nil 460 | }, 461 | { 462 | name: "Only spaces and commas", 463 | input: " , , ", 464 | expected: nil, // Changed from []string{} to nil 465 | }, 466 | } 467 | 468 | for _, tt := range tests { 469 | t.Run(tt.name, func(t *testing.T) { 470 | result := splitAndTrim(tt.input) 471 | if !reflect.DeepEqual(result, tt.expected) { 472 | t.Errorf("splitAndTrim(%q) = %v; want %v", tt.input, result, tt.expected) 473 | } 474 | }) 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /cmd/gofield/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | 12 | version "github.com/t34-dev/go-field-alignment/v2" 13 | ) 14 | 15 | // defaultFilePattern is the default regex pattern for files to process 16 | const defaultFilePattern = `\.go$` 17 | 18 | // main is the entry point of the program. 19 | // It handles command-line arguments, processes files based on the provided flags, 20 | // and applies necessary operations on the found files. 21 | func main() { 22 | // Init logging. 23 | // 24 | // Error logging will go to stderr. 25 | log.SetFlags(0) 26 | 27 | command := "" 28 | if len(os.Args) == 2 { 29 | command = strings.TrimSpace(os.Args[1]) 30 | } 31 | 32 | // Define flags 33 | filesFlag := flag.String("files", "", "Comma-separated list of files or folders to process") 34 | fFlag := flag.String("f", "", "Short form of --files") 35 | ignoreFlag := flag.String("ignore", "", "Comma-separated list of files or folders to ignore") 36 | iFlag := flag.String("i", "", "Short form of --ignore") 37 | viewFlag := flag.Bool("view", false, "Print the absolute paths of found files") 38 | vFlag := flag.Bool("v", false, "Short form of --view") 39 | fixFlag := flag.Bool("fix", false, "Make changes to the files") 40 | filePatternFlag := flag.String("pattern", "", "Regex pattern for files to process") 41 | ignorePatternFlag := flag.String("ignore-pattern", "", "Regex pattern for files to ignore") 42 | versionFlag := flag.Bool("version", false, "Print the version of the program") 43 | helpFlag := flag.Bool("help", false, "Print usage information") 44 | debugFlag := flag.Bool("debug", false, "Enable debug mode") 45 | 46 | // Parse flags 47 | flag.Parse() 48 | 49 | // Check for version flag 50 | if *versionFlag || command == "version" { 51 | fmt.Printf("Version: %s\n", version.Version) 52 | return 53 | } 54 | 55 | // Check for help flag or missing required flags 56 | if *helpFlag || command == "help" || (*filesFlag == "" && *fFlag == "") { 57 | printUsage() 58 | return 59 | } 60 | 61 | // Merge short and long form flags 62 | files := mergeFlags(*filesFlag, *fFlag) 63 | ignores := mergeFlags(*ignoreFlag, *iFlag) 64 | filePattern := *filePatternFlag 65 | ignorePattern := *ignorePatternFlag 66 | debugMode := *debugFlag 67 | fixMode := *fixFlag 68 | viewMode := *viewFlag || *vFlag 69 | 70 | // Ensure filePattern is not empty 71 | if filePattern == "" { 72 | filePattern = defaultFilePattern 73 | } 74 | 75 | // Compile regex patterns 76 | fileRegex, err := regexp.Compile(filePattern) 77 | if err != nil { 78 | log.Fatalf("Error compiling file pattern regex: %v\n", err) 79 | } 80 | 81 | var ignoreRegex *regexp.Regexp 82 | if ignorePattern != "" { 83 | ignoreRegex, err = regexp.Compile(ignorePattern) 84 | if err != nil { 85 | log.Fatalf("Error compiling ignore pattern regex: %v\n", err) 86 | } 87 | } 88 | 89 | ignoresMap, err := findFiles(ignores, fileRegex, ignoreRegex, nil) 90 | if err != nil { 91 | log.Fatalf("Cannot find files to ignore: %v\n", err) 92 | } 93 | filesToWork, err := findFiles(files, fileRegex, ignoreRegex, ignoresMap) 94 | if err != nil { 95 | log.Fatalf("Cannot find files to process: %v\n", err) 96 | } 97 | 98 | fmt.Printf("Files analyzed: %d\n-----------------\n", len(filesToWork)) 99 | 100 | allFiles := make([]string, 0, len(filesToWork)) 101 | for filePath := range filesToWork { 102 | allFiles = append(allFiles, filePath) 103 | } 104 | sort.Strings(allFiles) 105 | 106 | processingOpts := fileProcessingOptions{ 107 | viewMode: viewMode, 108 | fixMode: fixMode, 109 | debugMode: debugMode, 110 | } 111 | 112 | var filesToFix []string 113 | for _, filePath := range allFiles { 114 | needFix, err := processFile(filePath, processingOpts) 115 | if err != nil { 116 | log.Fatalf("Cannot process file '%s': %v\n", filePath, err) 117 | } 118 | if needFix { 119 | filesToFix = append(filesToFix, filePath) 120 | } 121 | } 122 | if len(filesToFix) == 0 { 123 | fmt.Printf("-----------------\nAll files are already optimized. No changes needed.\n") 124 | return 125 | } 126 | 127 | if fixMode { 128 | fmt.Printf("-----------------\nApplied fixes to %d files\n", len(filesToFix)) 129 | } else { 130 | fmt.Printf("-----------------\nFound files that need to be optimized:\n-- %s\n", strings.Join(filesToFix, "\n-- ")) 131 | os.Exit(1) 132 | } 133 | fmt.Println() 134 | } 135 | 136 | // printUsage prints the usage information for the program. 137 | func printUsage() { 138 | fmt.Println("Usage of gofield:") 139 | fmt.Println(" gofield --files [options]") 140 | fmt.Println("\nOptions:") 141 | fmt.Println(" --files, -f Comma-separated list of files or folders to process (required)") 142 | fmt.Println(" --ignore, -i Comma-separated list of files or folders to ignore") 143 | fmt.Println(" --view, -v Print the absolute paths of found files") 144 | fmt.Println(" --fix Make changes to the files") 145 | fmt.Println(" --pattern Regex pattern for files to process (default: \\.go$)") 146 | fmt.Println(" --ignore-pattern Regex pattern for files to ignore") 147 | fmt.Println(" --version Print the version of the program") 148 | fmt.Println(" --help Print this help message") 149 | fmt.Println("\nExamples:") 150 | fmt.Println(" gofield --files folder1,folder2 --ignore folder/ignore") 151 | fmt.Println(" gofield -f \"folder1, folder2/\" -i \"folder/ignore, folder2/ignore\"") 152 | fmt.Println(" gofield --files folder1 --pattern \"\\.(go|txt)$\" --view") 153 | fmt.Println(" gofield --files \"example, example, example/ignore\" --pattern \"(_test\\.go$|^filename_)\" --ignore-pattern \"_ignore\\.go$\" --view") 154 | fmt.Println(" gofield --files \"example, example/userx_test.go\" --ignore-pattern \"_test\\.go|ignore\\.go$\" -v") 155 | fmt.Println(" gofield --files \"example\"") 156 | fmt.Println(" gofield --files \"example\" --ignore-pattern \"_test\\.go$\"") 157 | fmt.Println(" gofield --files \"example\" --pattern \"_test\\.go$\"") 158 | fmt.Println(" gofield --files example --fix") 159 | } 160 | -------------------------------------------------------------------------------- /cmd/gofield/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | // testEnterFile is the path to the input test file 11 | const testEnterFile = "../../tests/enter/file.go" 12 | 13 | // testOutFile is the path to the expected output test file 14 | const testOutFile = "../../tests/out/file.go" 15 | 16 | // TestStructAlignment tests the alignment and optimization of struct fields. 17 | // It reads input and expected output files, applies the optimization, 18 | // and compares the result with the expected output. 19 | func TestStructAlignment(t *testing.T) { 20 | // read files 21 | enterFIle, err := os.ReadFile(testEnterFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | outFIle, err := os.ReadFile(testOutFile) 26 | // Normalize line endings to LF 27 | outFIle = normalizeLineEndings(outFIle) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | // parser 33 | structures, mapper, err := Parse(enterFIle) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | calculateStructures(structures, true) 38 | debugPrintStructures(structures) 39 | 40 | optimizeMapperStructures(mapper) 41 | calculateStructures(structures, false) 42 | debugPrintStructures(structures) 43 | 44 | renderTextStructures(structures) 45 | 46 | // Replace content 47 | resultFile, err := Replacer(enterFIle, structures) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | resultFile, err = format.Source(resultFile) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | // Compare modified code with structsSourceOut 58 | if !bytes.Equal(resultFile, outFIle) { 59 | t.Errorf("Modified code does not match expected output.\nGot:\n%s\nWant:\n%s", string(resultFile), string(outFIle)) 60 | } else { 61 | t.Log("Modified code matches expected output.") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/gofield/optimize.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | // ============= Optimization 9 | 10 | // optimizeStructure reorganizes the fields of a structure to minimize padding and optimize memory usage. 11 | // It sorts fields by alignment and size, separates regular fields from arrays and slices, 12 | // and recalculates field offsets for the optimized structure. 13 | func optimizeStructure(fields []*Structure) []*Structure { 14 | // Sort fields in descending order of alignment, then in descending order of size 15 | sort.Slice(fields, func(i, j int) bool { 16 | if fields[i].Align != fields[j].Align { 17 | return fields[i].Align > fields[j].Align 18 | } 19 | return fields[i].Size > fields[j].Size 20 | }) 21 | 22 | // Separately process arrays and slices 23 | var regularFields, arrayFields []*Structure 24 | for _, field := range fields { 25 | if strings.HasPrefix(field.StringType, "[") || strings.HasPrefix(field.StringType, "[]") { 26 | arrayFields = append(arrayFields, field) 27 | } else { 28 | regularFields = append(regularFields, field) 29 | } 30 | } 31 | 32 | // Merge back, placing arrays and slices at the end 33 | optimizedFields := append(regularFields, arrayFields...) 34 | 35 | // Recalculate offsets 36 | var currentOffset uintptr 37 | for i := range optimizedFields { 38 | currentOffset = align(currentOffset, optimizedFields[i].Align) 39 | optimizedFields[i].Offset = currentOffset 40 | currentOffset += optimizedFields[i].Size 41 | } 42 | 43 | return optimizedFields 44 | } 45 | 46 | // optimizeMapperStructures applies the optimizeStructure function to all structures in the given map. 47 | // It processes structures in order of their nesting depth (determined by the number of slashes in their path). 48 | func optimizeMapperStructures(mapStructures map[string]*Structure) { 49 | mapperItemsFlat := sortMapKeysBySlashCount(mapStructures) 50 | for _, structure := range mapperItemsFlat { 51 | if structure.IsStructure { 52 | structure.NestedFields = optimizeStructure(structure.NestedFields) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cmd/gofield/print.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ============= Print 9 | 10 | // debugPrintStructures iterates through a slice of Structure structures and prints each one. 11 | // It calls testPrintStructure for each element and adds a separator line between structures. 12 | func debugPrintStructures(structures []*Structure) { 13 | for _, elem := range structures { 14 | testPrintStructure(elem, 0) 15 | fmt.Println("-------------------------------------------") 16 | } 17 | } 18 | 19 | // testPrintStructure recursively prints the structure of an Structure element. 20 | // It formats the output to show field names, types, sizes, alignments, and offsets. 21 | // The function also calculates and displays padding between fields. 22 | func testPrintStructure(elem *Structure, tab int) { 23 | // alignment for beautiful display in logs 24 | maxFieldNameLength := 0 25 | maxTypeLength := 0 26 | 27 | if elem.IsStructure { 28 | for _, field := range elem.NestedFields { 29 | if len(field.Name) > maxFieldNameLength { 30 | maxFieldNameLength = len(field.Name) 31 | } 32 | if len(field.StringType) > maxTypeLength { 33 | maxTypeLength = len(field.StringType) 34 | } 35 | } 36 | infoFormat := fmt.Sprintf("%s %%-%ds %%-%ds %%s", strings.Repeat(" ", tab), maxValue(maxFieldNameLength, 5), maxValue(maxTypeLength, 11)) 37 | 38 | if tab == 0 { 39 | fmt.Printf("%stype %s struct {\n", strings.Repeat(" ", tab), elem.Name) 40 | } else { 41 | fmt.Printf("%s%s struct {\n", strings.Repeat(" ", tab), elem.Name) 42 | } 43 | var currentOffset uintptr 44 | for idx, field := range elem.NestedFields { 45 | isValidCustomNameType := isValidCustomTypeName(field.StringType) 46 | 47 | if field.IsStructure && !isValidCustomNameType { 48 | testPrintStructure(field, tab+4) 49 | currentOffset += field.Size 50 | } else { 51 | str := fmt.Sprintf("[Size: %d, Align: %d, Offset: %d]", field.Size, field.Align, field.Offset) 52 | padding := field.Offset - currentOffset 53 | if padding > 0 { 54 | str = fmt.Sprintf("+%db %s", padding, str) 55 | } 56 | currentOffset = field.Offset + field.Size 57 | if idx == len(elem.NestedFields)-1 { 58 | finalPadding := elem.Size - currentOffset 59 | if finalPadding > 0 { 60 | str = fmt.Sprintf("%s +%db", str, finalPadding) 61 | } 62 | } 63 | fmt.Printf(infoFormat+"\n", field.Name, field.StringType, str) 64 | } 65 | } 66 | } 67 | 68 | fmt.Printf("%s} [Size: %d, Align: %d, Offset: %d]\n", strings.Repeat(" ", tab), elem.Size, elem.Align, elem.Offset) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/gofield/render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "strings" 7 | ) 8 | 9 | // ============= Render 10 | 11 | // renderStructure generates a string representation of an Structure structure. 12 | // It handles both top-level structures and nested fields, including their 13 | // documentation, tags, and comments. The function recursively processes 14 | // nested structures to create a complete representation. 15 | // 16 | // The function performs the following tasks: 17 | // - Checks if the element is a valid custom type or a structure 18 | // - Generates the struct definition with its name (for top-level structures) 19 | // - Iterates through all nested fields, rendering each one 20 | // - Includes field documentation, tags, and comments 21 | // - Handles both root-level and nested comments 22 | // 23 | // Parameters: 24 | // - elem: Pointer to an Structure structure to be rendered 25 | // 26 | // Returns: 27 | // - A string containing the rendered structure 28 | func renderStructure(elem *Structure) string { 29 | isValidCustomNameType := isValidCustomTypeName(elem.StringType) 30 | if !elem.IsStructure || isValidCustomNameType { 31 | return elem.StringType 32 | } 33 | 34 | var data strings.Builder 35 | if elem.Root != nil { 36 | data.WriteString(elem.Name) 37 | 38 | // Render generic type params 39 | if typeParams := elem.Root.TypeParams; typeParams != nil && len(typeParams.List) > 0 { 40 | params := typeParams.List 41 | data.WriteRune('[') 42 | data.WriteString(renderTypeParameter(params[0])) 43 | for _, p := range params[1:] { 44 | data.WriteString(", ") 45 | data.WriteString(renderTypeParameter(p)) 46 | } 47 | data.WriteRune(']') 48 | } 49 | 50 | // Don't add "type " here, as current structure may be inside a "type" block 51 | data.WriteString(" struct {") 52 | } else { 53 | // Anonymous structs don't support type params 54 | data.WriteString("struct {") 55 | } 56 | if len(elem.NestedFields) > 0 { 57 | data.WriteRune('\n') 58 | } 59 | 60 | for idx, field := range elem.NestedFields { 61 | // Doc 62 | if field.RootField != nil && field.RootField.Doc != nil && len(field.RootField.Doc.List) > 0 { 63 | for _, comment := range field.RootField.Doc.List { 64 | data.WriteString(comment.Text) 65 | data.WriteRune('\n') 66 | } 67 | } 68 | if strings.HasPrefix(field.Name, "!") { 69 | field.Name = "" 70 | } 71 | data.WriteString(fmt.Sprintf("%s %s ", field.Name, renderStructure(field))) 72 | // Tag 73 | if field.RootField != nil { 74 | // Tags 75 | if field.RootField.Tag != nil && len(field.RootField.Tag.Value) > 0 { 76 | data.WriteString(fmt.Sprintf(" %s", field.RootField.Tag.Value)) 77 | } 78 | // Comment 79 | if field.RootField.Comment != nil && len(field.RootField.Comment.List) > 0 { 80 | for _, comment := range field.RootField.Comment.List { 81 | data.WriteString(fmt.Sprintf(" %s", comment.Text)) 82 | } 83 | } 84 | } 85 | if idx != len(elem.NestedFields) { 86 | data.WriteRune('\n') 87 | } 88 | } 89 | 90 | data.WriteRune('}') 91 | 92 | // Comments 93 | if elem.RootField != nil { 94 | if elem.RootField.Comment != nil && len(elem.RootField.Comment.List) > 0 { 95 | for _, comment := range elem.RootField.Comment.List { 96 | data.WriteString(comment.Text) 97 | } 98 | } 99 | } else if elem.Root != nil { 100 | if elem.Root.Comment != nil && len(elem.Root.Comment.List) > 0 { 101 | for _, comment := range elem.Root.Comment.List { 102 | data.WriteString(comment.Text) 103 | } 104 | } 105 | } 106 | return data.String() 107 | } 108 | 109 | // renderTypeParameter renders the given type parameter as Go code. 110 | func renderTypeParameter(f *ast.Field) string { 111 | names := make([]string, len(f.Names)) 112 | for i := 0; i < len(names); i++ { 113 | names[i] = f.Names[i].Name 114 | } 115 | return fmt.Sprintf("%s %s", strings.Join(names, ", "), getTypeString(f.Type)) 116 | } 117 | 118 | func renderTextStructures(structures []*Structure) { 119 | for _, structure := range structures { 120 | // Don't format code here - "renderStructure" generates a replacement for a part of target Go file, 121 | // not a valid piece of Go code per-se. 122 | // 123 | // Code will be formatted afterwards. 124 | structure.MetaData.Data = []byte(renderStructure(structure)) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cmd/gofield/size.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "unsafe" 7 | ) 8 | 9 | // getTypeID generates a unique identifier for an AST expression. 10 | // This is used to detect recursive types and prevent infinite loops. 11 | func getTypeID(expr ast.Expr) string { 12 | return fmt.Sprintf("%T:%p", expr, expr) 13 | } 14 | 15 | // getFieldSizeWithMap calculates the size of a field in a structure. 16 | // It handles various types including basic types, pointers, arrays, structs, maps, channels, and interfaces. 17 | // The function uses a map to keep track of seen types to handle recursive structures. 18 | func getFieldSizeWithMap(field ast.Expr, seenTypes map[string]bool) uintptr { 19 | typeID := getTypeID(field) 20 | 21 | if seenTypes[typeID] { 22 | return unsafe.Sizeof(uintptr(0)) 23 | } 24 | 25 | seenTypes[typeID] = true 26 | defer delete(seenTypes, typeID) 27 | 28 | switch t := (field).(type) { 29 | case *ast.Ident: 30 | switch t.Name { 31 | case "bool": 32 | return unsafe.Sizeof(bool(false)) 33 | case "int8", "uint8", "byte": 34 | return unsafe.Sizeof(int8(0)) 35 | case "int16", "uint16": 36 | return unsafe.Sizeof(int16(0)) 37 | case "int32", "uint32", "float32", "rune": 38 | return unsafe.Sizeof(int32(0)) 39 | case "int64", "uint64", "float64": 40 | return unsafe.Sizeof(int64(0)) 41 | case "int", "uint": 42 | return unsafe.Sizeof(int(0)) 43 | case "string": 44 | return unsafe.Sizeof("") 45 | case "complex64": 46 | return unsafe.Sizeof(complex64(0)) 47 | case "complex128": 48 | return unsafe.Sizeof(complex128(0)) 49 | } 50 | case *ast.StarExpr: 51 | return unsafe.Sizeof(uintptr(0)) 52 | case *ast.ArrayType: 53 | if t.Len == nil { 54 | return unsafe.Sizeof([]int{}) 55 | } else { 56 | elemSize := getFieldSizeWithMap(t.Elt, seenTypes) 57 | length := 0 58 | if lit, ok := t.Len.(*ast.BasicLit); ok { 59 | fmt.Sscanf(lit.Value, "%d", &length) 60 | } 61 | return elemSize * uintptr(length) // Remove padding 62 | } 63 | case *ast.StructType: 64 | var size, maxAlign uintptr 65 | for _, field := range t.Fields.List { 66 | fieldSize := getFieldSizeWithMap(field.Type, seenTypes) 67 | fieldAlign := getFieldAlign(field.Type) 68 | size = align(size, fieldAlign) + fieldSize 69 | if fieldAlign > maxAlign { 70 | maxAlign = fieldAlign 71 | } 72 | } 73 | return align(size, maxAlign) 74 | case *ast.MapType: 75 | return unsafe.Sizeof(map[string]int{}) 76 | case *ast.ChanType: 77 | return unsafe.Sizeof(make(chan int)) 78 | case *ast.InterfaceType: 79 | return unsafe.Sizeof((*interface{})(nil)) 80 | } 81 | return unsafe.Sizeof("") 82 | } 83 | 84 | // getFieldAlign determines the alignment requirement of a field. 85 | // It handles various types similar to getFieldSizeWithMap. 86 | func getFieldAlign(field ast.Expr) uintptr { 87 | switch t := (field).(type) { 88 | case *ast.Ident: 89 | switch t.Name { 90 | case "bool": 91 | return unsafe.Alignof(false) 92 | case "int8", "uint8", "byte": 93 | return unsafe.Alignof(int8(0)) 94 | case "int16", "uint16": 95 | return unsafe.Alignof(int16(0)) 96 | case "int32", "uint32", "float32", "rune": 97 | return unsafe.Alignof(int32(0)) 98 | case "int64", "uint64", "float64": 99 | return unsafe.Alignof(int64(0)) 100 | case "int", "uint": 101 | return unsafe.Alignof(0) 102 | case "string": 103 | return unsafe.Alignof("") 104 | } 105 | case *ast.StarExpr: 106 | return unsafe.Alignof(uintptr(0)) 107 | case *ast.ArrayType: 108 | return getFieldAlign(t.Elt) 109 | case *ast.StructType: 110 | var maxAlign uintptr 111 | for _, field := range t.Fields.List { 112 | fieldAlign := getFieldAlign(field.Type) 113 | if fieldAlign > maxAlign { 114 | maxAlign = fieldAlign 115 | } 116 | } 117 | return maxAlign 118 | case *ast.MapType: 119 | return unsafe.Alignof(map[string]int{}) 120 | case *ast.ChanType: 121 | return unsafe.Alignof(make(chan int)) 122 | case *ast.InterfaceType: 123 | return unsafe.Alignof((*interface{})(nil)) 124 | } 125 | return unsafe.Alignof("") 126 | } 127 | 128 | // align calculates the next aligned address given a size and an alignment. 129 | // This function is used to ensure proper alignment of fields within a structure. 130 | func align(size, align uintptr) uintptr { 131 | return (size + align - 1) &^ (align - 1) 132 | } 133 | 134 | // getFieldSize is a wrapper function that initializes a new map and calls getFieldSizeWithMap. 135 | // This function is the main entry point for calculating field sizes. 136 | func getFieldSize(field ast.Expr) uintptr { 137 | return getFieldSizeWithMap(field, make(map[string]bool)) 138 | } 139 | -------------------------------------------------------------------------------- /cmd/gofield/size_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "reflect" 7 | "testing" 8 | "unsafe" 9 | ) 10 | 11 | // TestGetFieldSizeStruct tests the getFieldSize function with a simple struct. 12 | // It compares the calculated size with the actual size of an equivalent Go struct. 13 | func TestGetFieldSizeStruct(t *testing.T) { 14 | structExpr := &ast.StructType{ 15 | Fields: &ast.FieldList{ 16 | List: []*ast.Field{ 17 | {Type: &ast.Ident{Name: "int"}}, 18 | {Type: &ast.Ident{Name: "string"}}, 19 | {Type: &ast.Ident{Name: "bool"}}, 20 | }, 21 | }, 22 | } 23 | 24 | var expr ast.Expr = structExpr 25 | size := getFieldSize(expr) 26 | 27 | // Create a real structure for comparison 28 | type testStruct struct { 29 | i int 30 | s string 31 | b bool 32 | } 33 | expected := reflect.TypeOf(testStruct{}).Size() 34 | 35 | // Output detailed information about sizes and alignment 36 | t.Logf("Size of int: %d", unsafe.Sizeof(int(0))) 37 | t.Logf("Size of string: %d", unsafe.Sizeof("")) 38 | t.Logf("Size of bool: %d", unsafe.Sizeof(false)) 39 | t.Logf("Structure alignment: %d", reflect.TypeOf(testStruct{}).Align()) 40 | 41 | v := reflect.ValueOf(testStruct{}) 42 | for i := 0; i < v.NumField(); i++ { 43 | field := v.Type().Field(i) 44 | t.Logf("Field %s: size %d, offset %d, alignment %d", 45 | field.Name, field.Type.Size(), field.Offset, field.Type.Align()) 46 | } 47 | 48 | t.Logf("Expected size: %d", expected) 49 | t.Logf("Actual size: %d", size) 50 | 51 | if size != expected { 52 | t.Errorf("getFieldSize() for struct = %v, want %v", size, expected) 53 | } 54 | } 55 | 56 | // TestGetFieldSizeTypes tests the getFieldSize function for various basic types. 57 | // It checks if the calculated sizes match the actual sizes of Go types. 58 | func TestGetFieldSizeTypes(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | expr ast.Expr 62 | want uintptr 63 | }{ 64 | {"bool", &ast.Ident{Name: "bool"}, unsafe.Sizeof(bool(false))}, 65 | {"int", &ast.Ident{Name: "int"}, unsafe.Sizeof(int(0))}, 66 | {"int8", &ast.Ident{Name: "int8"}, unsafe.Sizeof(int8(0))}, 67 | {"int16", &ast.Ident{Name: "int16"}, unsafe.Sizeof(int16(0))}, 68 | {"int32", &ast.Ident{Name: "int32"}, unsafe.Sizeof(int32(0))}, 69 | {"int64", &ast.Ident{Name: "int64"}, unsafe.Sizeof(int64(0))}, 70 | {"uint", &ast.Ident{Name: "uint"}, unsafe.Sizeof(uint(0))}, 71 | {"uint8", &ast.Ident{Name: "uint8"}, unsafe.Sizeof(uint8(0))}, 72 | {"uint16", &ast.Ident{Name: "uint16"}, unsafe.Sizeof(uint16(0))}, 73 | {"uint32", &ast.Ident{Name: "uint32"}, unsafe.Sizeof(uint32(0))}, 74 | {"uint64", &ast.Ident{Name: "uint64"}, unsafe.Sizeof(uint64(0))}, 75 | {"float32", &ast.Ident{Name: "float32"}, unsafe.Sizeof(float32(0))}, 76 | {"float64", &ast.Ident{Name: "float64"}, unsafe.Sizeof(float64(0))}, 77 | {"complex64", &ast.Ident{Name: "complex64"}, unsafe.Sizeof(complex64(0))}, 78 | {"complex128", &ast.Ident{Name: "complex128"}, unsafe.Sizeof(complex128(0))}, 79 | {"string", &ast.Ident{Name: "string"}, unsafe.Sizeof("")}, 80 | {"slice", &ast.ArrayType{Elt: &ast.Ident{Name: "int"}}, unsafe.Sizeof([]int{})}, 81 | {"array", &ast.ArrayType{Elt: &ast.Ident{Name: "int"}, Len: &ast.BasicLit{Kind: token.INT, Value: "5"}}, unsafe.Sizeof([5]int{})}, 82 | {"map", &ast.MapType{Key: &ast.Ident{Name: "string"}, Value: &ast.Ident{Name: "int"}}, unsafe.Sizeof(map[string]int{})}, 83 | {"chan", &ast.ChanType{Value: &ast.Ident{Name: "int"}}, unsafe.Sizeof(make(chan int))}, 84 | {"interface", &ast.InterfaceType{}, unsafe.Sizeof((*interface{})(nil))}, 85 | } 86 | 87 | for _, tt := range tests { 88 | t.Run(tt.name, func(t *testing.T) { 89 | got := getFieldSize(tt.expr) 90 | if got != tt.want { 91 | t.Errorf("getFieldSize() = %v, want %v", got, tt.want) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | // TestGetFieldSizePointers tests the getFieldSize function for pointer types. 98 | // It verifies that the function correctly calculates sizes for single and double pointers. 99 | func TestGetFieldSizePointers(t *testing.T) { 100 | tests := []struct { 101 | name string 102 | expr ast.Expr 103 | want uintptr 104 | }{ 105 | {"*int", &ast.StarExpr{X: &ast.Ident{Name: "int"}}, unsafe.Sizeof((*int)(nil))}, 106 | {"*string", &ast.StarExpr{X: &ast.Ident{Name: "string"}}, unsafe.Sizeof((*string)(nil))}, 107 | {"*bool", &ast.StarExpr{X: &ast.Ident{Name: "bool"}}, unsafe.Sizeof((*bool)(nil))}, 108 | {"**int", &ast.StarExpr{X: &ast.StarExpr{X: &ast.Ident{Name: "int"}}}, unsafe.Sizeof((**int)(nil))}, 109 | } 110 | 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | if got := getFieldSize(tt.expr); got != tt.want { 114 | t.Errorf("getFieldSize() = %v, want %v", got, tt.want) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | // TestGetFieldSizeComplexStructs tests the getFieldSize function for more complex struct types. 121 | // It includes tests for structs with embedded structs, slices, and maps. 122 | func TestGetFieldSizeComplexStructs(t *testing.T) { 123 | tests := []struct { 124 | name string 125 | expr ast.Expr 126 | want uintptr 127 | }{ 128 | { 129 | name: "struct with embedded struct", 130 | expr: &ast.StructType{ 131 | Fields: &ast.FieldList{ 132 | List: []*ast.Field{ 133 | {Type: &ast.Ident{Name: "int"}}, 134 | {Type: &ast.StructType{ 135 | Fields: &ast.FieldList{ 136 | List: []*ast.Field{ 137 | {Type: &ast.Ident{Name: "string"}}, 138 | {Type: &ast.Ident{Name: "bool"}}, 139 | }, 140 | }, 141 | }}, 142 | }, 143 | }, 144 | }, 145 | want: reflect.TypeOf(struct { 146 | i int 147 | s struct { 148 | str string 149 | b bool 150 | } 151 | }{}).Size(), 152 | }, 153 | { 154 | name: "struct with slice and map", 155 | expr: &ast.StructType{ 156 | Fields: &ast.FieldList{ 157 | List: []*ast.Field{ 158 | {Type: &ast.ArrayType{Elt: &ast.Ident{Name: "int"}}}, 159 | {Type: &ast.MapType{Key: &ast.Ident{Name: "string"}, Value: &ast.Ident{Name: "int"}}}, 160 | }, 161 | }, 162 | }, 163 | want: reflect.TypeOf(struct { 164 | s []int 165 | m map[string]int 166 | }{}).Size(), 167 | }, 168 | } 169 | 170 | for _, tt := range tests { 171 | t.Run(tt.name, func(t *testing.T) { 172 | if got := getFieldSize(tt.expr); got != tt.want { 173 | t.Errorf("getFieldSize() = %v, want %v", got, tt.want) 174 | } 175 | }) 176 | } 177 | } 178 | 179 | // TestGetFieldSizeRecursiveStruct tests the getFieldSize function with a recursive struct. 180 | // It verifies that the function can handle recursive types without infinite loops. 181 | func TestGetFieldSizeRecursiveStruct(t *testing.T) { 182 | // Define a recursive structure 183 | type RecursiveStruct struct { 184 | i int 185 | r *RecursiveStruct 186 | } 187 | 188 | // Create AST representation of RecursiveStruct 189 | recursiveStructExpr := &ast.StructType{ 190 | Fields: &ast.FieldList{ 191 | List: []*ast.Field{ 192 | { 193 | Names: []*ast.Ident{{Name: "i"}}, 194 | Type: &ast.Ident{Name: "int"}, 195 | }, 196 | { 197 | Names: []*ast.Ident{{Name: "r"}}, 198 | Type: &ast.StarExpr{ 199 | X: &ast.Ident{Name: "RecursiveStruct"}, 200 | }, 201 | }, 202 | }, 203 | }, 204 | } 205 | 206 | // Create full AST representation of the type definition 207 | typeSpec := &ast.TypeSpec{ 208 | Name: &ast.Ident{Name: "RecursiveStruct"}, 209 | Type: recursiveStructExpr, 210 | } 211 | 212 | // Wrap the type definition in GenDecl, as it would be in a real Go file 213 | genDecl := &ast.GenDecl{ 214 | Tok: token.TYPE, 215 | Specs: []ast.Spec{typeSpec}, 216 | } 217 | 218 | // Now we have a complete AST representation of the RecursiveStruct type definition 219 | 220 | var expr ast.Expr = recursiveStructExpr 221 | size := getFieldSize(expr) 222 | 223 | expected := reflect.TypeOf(RecursiveStruct{}).Size() 224 | 225 | t.Logf("AST representation of the structure:") 226 | t.Logf("%#v", genDecl) 227 | t.Logf("Size of int: %d", unsafe.Sizeof(int(0))) 228 | t.Logf("Size of pointer: %d", unsafe.Sizeof(uintptr(0))) 229 | t.Logf("Expected size: %d", expected) 230 | t.Logf("Actual size: %d", size) 231 | 232 | if size != expected { 233 | t.Errorf("getFieldSize() for recursive struct = %v, want %v", size, expected) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /cmd/gofield/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | 10 | textreplacer "github.com/t34-dev/go-text-replacer" 11 | ) 12 | 13 | // MetaData represents the outcome of struct optimization 14 | type MetaData struct { 15 | BeforeSize uintptr 16 | AfterSize uintptr 17 | Data []byte 18 | StartPos int 19 | EndPos int 20 | } 21 | 22 | // Structure represents detailed information about a struct field or type 23 | type Structure struct { 24 | Name string 25 | Path string 26 | Root *ast.TypeSpec 27 | RootField *ast.Field 28 | StructType ast.Expr 29 | StringType string 30 | IsStructure bool 31 | Size uintptr 32 | Align uintptr 33 | Offset uintptr 34 | NestedFields []*Structure 35 | MetaData *MetaData 36 | } 37 | 38 | // ParseFile parses a Go file and returns optimization results 39 | func ParseFile(path string) ([]*Structure, map[string]*Structure, error) { 40 | return parseData(path, nil) 41 | } 42 | 43 | // Parse parses Go code from a byte slice and returns optimization results 44 | func Parse(bytes []byte) ([]*Structure, map[string]*Structure, error) { 45 | return parseData("", bytes) 46 | } 47 | 48 | // ParseWithFilename parses Go code from a byte slice with a filename and returns optimization results 49 | func ParseWithFilename(filename string, bytes []byte) ([]*Structure, map[string]*Structure, error) { 50 | return parseData(filename, bytes) 51 | } 52 | 53 | // ParseStrings parses Go code from a string and returns optimization results 54 | func ParseStrings(str string) ([]*Structure, map[string]*Structure, error) { 55 | return parseData("", []byte(str)) 56 | } 57 | 58 | // parseData is the core function that handles parsing and optimization of Go code 59 | func parseData(path string, bytes []byte) ([]*Structure, map[string]*Structure, error) { 60 | // Normalize line endings to LF 61 | bytes = normalizeLineEndings(bytes) 62 | 63 | node, err := parser.ParseFile(token.NewFileSet(), path, bytes, parser.ParseComments) 64 | if err != nil { 65 | if path == "" { 66 | return nil, nil, errors.New(fmt.Sprintf("Failed to parseData source: %v", err)) 67 | } 68 | return nil, nil, errors.New(fmt.Sprintf("Failed to parseData source %s: %v", path, err)) 69 | } 70 | 71 | //var results []MetaData 72 | var structures []*Structure 73 | mapperItems := map[string]*Structure{} 74 | 75 | ast.Inspect(node, func(n ast.Node) bool { 76 | typeSpec, ok := n.(*ast.TypeSpec) 77 | if !ok { 78 | return true 79 | } 80 | _, ok = typeSpec.Type.(*ast.StructType) 81 | if !ok { 82 | return true 83 | } 84 | // Don't subtract len("type ") here - this leads to incorrect start pos 85 | // in cases when struct is in a "type" block (type (...)). 86 | startPos := int(typeSpec.Pos()) 87 | plus := 0 88 | if typeSpec.Comment != nil && len(typeSpec.Comment.List) > 0 { 89 | plus += len(typeSpec.Comment.List[0].Text) + 1 90 | } 91 | endPos := int(typeSpec.Type.End()) + plus 92 | metaData := MetaData{ 93 | StartPos: startPos, 94 | EndPos: endPos, 95 | } 96 | item := createTypeItemInfo(typeSpec, nil, mapperItems) 97 | item.MetaData = &metaData 98 | if item != nil { 99 | structures = append(structures, item) 100 | } 101 | return true 102 | }) 103 | return structures, mapperItems, err 104 | } 105 | 106 | func createMapperItem(structure *Structure, mapperItems map[string]*Structure) map[string]*Structure { 107 | mapperItems[structure.Path] = structure 108 | if structure.IsStructure { 109 | for _, elem := range structure.NestedFields { 110 | createMapperItem(elem, mapperItems) 111 | } 112 | } 113 | return mapperItems 114 | } 115 | 116 | func createMapper(structures []*Structure) map[string]*Structure { 117 | mapper := map[string]*Structure{} 118 | for _, structure := range structures { 119 | createMapperItem(structure, mapper) 120 | } 121 | return mapper 122 | } 123 | 124 | // Replacer replaces the original struct definitions with optimized versions in the source code 125 | func Replacer(file []byte, structures []*Structure) ([]byte, error) { 126 | // Normalize line endings to LF 127 | file = normalizeLineEndings(file) 128 | 129 | var blocks []textreplacer.Block 130 | for _, elem := range structures { 131 | blocks = append(blocks, textreplacer.Block{ 132 | Start: elem.MetaData.StartPos - 1, 133 | End: elem.MetaData.EndPos - 1, 134 | Txt: elem.MetaData.Data, 135 | }) 136 | } 137 | replacer := textreplacer.New(file) 138 | return replacer.Enter(blocks) 139 | } 140 | -------------------------------------------------------------------------------- /cmd/gofield/types_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | // BadStruct represents an inefficiently aligned structure 12 | type BadStruct struct { 13 | a bool // 1 byte 14 | b int32 // 4 bytes 15 | c bool // 1 byte 16 | d int64 // 8 bytes 17 | } 18 | 19 | // GoodStruct represents an efficiently aligned version of BadStruct 20 | type GoodStruct struct { 21 | d int64 // 8 bytes 22 | b int32 // 4 bytes 23 | a bool // 1 byte 24 | c bool // 1 byte 25 | } 26 | 27 | // TestBadStructAlignment checks if the BadStruct has the expected memory layout and size. 28 | // It verifies that the struct is not optimally aligned, resulting in a larger size. 29 | func TestBadStructAlignment(t *testing.T) { 30 | badStruct := BadStruct{} 31 | size := reflect.TypeOf(badStruct).Size() 32 | expectedSize := uintptr(24) // 1 + 4 + 1 + 8 + padding = 24 bytes on most systems 33 | 34 | if size != expectedSize { 35 | t.Errorf("BadStruct size = %d; want %d", size, expectedSize) 36 | } 37 | } 38 | 39 | // TestGoodStructAlignment checks if the GoodStruct has the expected memory layout and size. 40 | // It verifies that the struct is optimally aligned, resulting in a smaller size. 41 | func TestGoodStructAlignment(t *testing.T) { 42 | goodStruct := GoodStruct{} 43 | size := reflect.TypeOf(goodStruct).Size() 44 | expectedSize := uintptr(16) // 8 + 4 + 1 + 1 + padding = 16 bytes on most systems 45 | 46 | if size != expectedSize { 47 | t.Errorf("GoodStruct size = %d; want %d", size, expectedSize) 48 | } 49 | } 50 | 51 | // TestParseStrings tests the ParseStrings function. 52 | // It checks if the function correctly parses a string containing a struct definition, 53 | // and if it produces the expected optimization results. 54 | func TestParseStrings(t *testing.T) { 55 | input := `package main 56 | 57 | type TestStruct struct { 58 | a bool 59 | b int64 60 | c bool 61 | } 62 | ` 63 | results, mapperData, err := ParseStrings(input) 64 | if err != nil { 65 | t.Fatalf("ParseStrings failed: %v", err) 66 | } 67 | 68 | if len(results) != 1 { 69 | t.Fatalf("Expected 1 result, got %d", len(results)) 70 | } 71 | 72 | calculateStructures(results, true) 73 | optimizeMapperStructures(mapperData) 74 | calculateStructures(results, false) 75 | renderTextStructures(results) 76 | 77 | if results[0].Name != "TestStruct" { 78 | t.Errorf("Expected struct name 'TestStruct', got '%s'", results[0].Name) 79 | } 80 | 81 | // Check that sizes were calculated 82 | if results[0].MetaData.BeforeSize == 0 || results[0].MetaData.AfterSize == 0 { 83 | t.Errorf("Expected non-zero sizes, got before: %d, after: %d", results[0].MetaData.BeforeSize, results[0].MetaData.AfterSize) 84 | } 85 | 86 | replaced, err := Replacer([]byte(input), results) 87 | if err != nil { 88 | t.Errorf("Cannot replace structs code: %v\n", err) 89 | } 90 | formatted, err := format.Source(replaced) 91 | if err != nil { 92 | t.Errorf("Cannot format generated data: %v\n", err) 93 | } 94 | 95 | // Check that the field order has changed (optimization) 96 | optimizedStructString := string(formatted) 97 | if !strings.Contains(optimizedStructString, "b int64\n\ta bool\n\tc bool") { 98 | t.Errorf("Expected fields to be reordered, got: %s", optimizedStructString) 99 | } 100 | } 101 | 102 | // TestParseBytes tests the Parse function. 103 | // It verifies that the function can correctly parseData a byte slice containing 104 | // a struct definition and produce the expected results. 105 | func TestParseBytes(t *testing.T) { 106 | input := []byte(`package main 107 | 108 | type TestStruct struct { 109 | a bool 110 | b int32 111 | c string 112 | } 113 | `) 114 | results, _, err := Parse(input) 115 | if err != nil { 116 | t.Fatalf("Parse failed: %v", err) 117 | } 118 | 119 | if len(results) != 1 { 120 | t.Fatalf("Expected 1 result, got %d", len(results)) 121 | } 122 | 123 | if results[0].Name != "TestStruct" { 124 | t.Errorf("Expected struct name 'TestStruct', got '%s'", results[0].Name) 125 | } 126 | } 127 | 128 | // TestReplacer tests the Replacer function. 129 | // It checks if the function can replace the original struct definition 130 | // with an optimized version. 131 | func TestReplacer(t *testing.T) { 132 | original := []byte(`package main 133 | 134 | type TestStruct struct { 135 | a bool 136 | b int64 137 | c bool 138 | } 139 | `) 140 | results, mapperData, err := Parse(original) 141 | if err != nil { 142 | t.Fatalf("Parse failed: %v", err) 143 | } 144 | calculateStructures(results, true) 145 | optimizeMapperStructures(mapperData) 146 | calculateStructures(results, false) 147 | renderTextStructures(results) 148 | 149 | modified, err := Replacer(original, results) 150 | if err != nil { 151 | t.Fatalf("Replacer failed: %v", err) 152 | } 153 | 154 | modified, err = format.Source(modified) 155 | if err != nil { 156 | t.Fatalf("Formatting failed: %v", err) 157 | } 158 | 159 | if bytes.Equal(original, modified) { 160 | t.Errorf("Expected modified content to be different from original") 161 | } 162 | 163 | if !bytes.Contains(modified, []byte("TestStruct")) { 164 | t.Errorf("Modified content should still contain 'TestStruct'") 165 | } 166 | 167 | //Check that the field order has changed 168 | if !bytes.Contains(modified, []byte("b int64\n\ta bool\n\tc bool")) { 169 | t.Errorf("Expected fields to be reordered in the modified content") 170 | } 171 | } 172 | 173 | // TestParseFile is a mock test for the ParseFile function. 174 | // Since ParseFile depends on the file system, this test only checks 175 | // if the function exists and returns an expected error for a non-existent file. 176 | func TestParseFile(t *testing.T) { 177 | // In a real scenario, you would create a temporary file for testing 178 | // Here we just check that the function exists and returns the expected type 179 | _, _, err := ParseFile("non_existent_file.go") 180 | if err == nil { 181 | t.Errorf("Expected error for non-existent file, got nil") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /cmd/gofield/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "sort" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | // stdTypes is a map of standard Go types used for type checking 12 | var stdTypes = map[string]bool{ 13 | "bool": true, 14 | "string": true, 15 | "int": true, 16 | "int8": true, 17 | "int16": true, 18 | "int32": true, 19 | "int64": true, 20 | "uint": true, 21 | "uint8": true, 22 | "uint16": true, 23 | "uint32": true, 24 | "uint64": true, 25 | "uintptr": true, 26 | "byte": true, 27 | "rune": true, 28 | "float32": true, 29 | "float64": true, 30 | "complex64": true, 31 | "complex128": true, 32 | } 33 | 34 | // getTypeString returns a string representation of an AST expression 35 | func getTypeString(expr ast.Expr) string { 36 | if expr == nil { 37 | return "" 38 | } 39 | 40 | switch t := expr.(type) { 41 | case *ast.Ident: 42 | return t.Name 43 | case *ast.BasicLit: 44 | return t.Value 45 | case *ast.StarExpr: 46 | return "*" + getTypeString(t.X) 47 | case *ast.ArrayType: 48 | if t.Len == nil { 49 | return "[]" + getTypeString(t.Elt) 50 | } 51 | return fmt.Sprintf("[%s]%s", getTypeString(t.Len), getTypeString(t.Elt)) 52 | case *ast.SelectorExpr: 53 | return getTypeString(t.X) + "." + t.Sel.Name 54 | case *ast.FuncType: 55 | return getFuncTypeString(t) 56 | case *ast.MapType: 57 | return fmt.Sprintf("map[%s]%s", getTypeString(t.Key), getTypeString(t.Value)) 58 | case *ast.ChanType: 59 | return getChanTypeString(t) 60 | case *ast.StructType: 61 | return "struct{}" 62 | case *ast.InterfaceType: 63 | return "interface{}" 64 | case *ast.Ellipsis: 65 | return "..." + getTypeString(t.Elt) 66 | case *ast.ParenExpr: 67 | return "(" + getTypeString(t.X) + ")" 68 | case *ast.CompositeLit: 69 | return getTypeString(t.Type) 70 | case *ast.FuncLit: 71 | return getFuncTypeString(t.Type) 72 | case *ast.IndexExpr: 73 | // Single type argument 74 | return fmt.Sprintf("%s[%s]", getTypeString(t.X), getTypeString(t.Index)) 75 | case *ast.IndexListExpr: 76 | // Multiple type arguments 77 | idxTypes := make([]string, len(t.Indices)) 78 | for i, idx := range t.Indices { 79 | idxTypes[i] = getTypeString(idx) 80 | } 81 | return fmt.Sprintf("%s[%s]", getTypeString(t.X), strings.Join(idxTypes, ", ")) 82 | default: 83 | // TODO: maybe, it's better to fail here? 84 | // 85 | // It'll be easier to add support of what we've missed. 86 | return fmt.Sprintf("%T", expr) 87 | } 88 | } 89 | 90 | // getFuncTypeString returns a string representation of a function type 91 | func getFuncTypeString(t *ast.FuncType) string { 92 | params := getFieldListString(t.Params) 93 | results := getFieldListString(t.Results) 94 | 95 | if results == "" { 96 | return fmt.Sprintf("func(%s)", params) 97 | } 98 | return fmt.Sprintf("func(%s) %s", params, results) 99 | } 100 | 101 | // getChanTypeString returns a string representation of a channel type 102 | func getChanTypeString(t *ast.ChanType) string { 103 | switch t.Dir { 104 | case ast.SEND: 105 | return fmt.Sprintf("chan<- %s", getTypeString(t.Value)) 106 | case ast.RECV: 107 | return fmt.Sprintf("<-chan %s", getTypeString(t.Value)) 108 | default: 109 | return fmt.Sprintf("chan %s", getTypeString(t.Value)) 110 | } 111 | } 112 | 113 | // getFieldListString returns a string representation of a field list 114 | func getFieldListString(fields *ast.FieldList) string { 115 | if fields == nil { 116 | return "" 117 | } 118 | var parts []string 119 | for _, field := range fields.List { 120 | typeStr := getTypeString(field.Type) 121 | if len(field.Names) > 0 { 122 | for _, name := range field.Names { 123 | parts = append(parts, fmt.Sprintf("%s %s", name.Name, typeStr)) 124 | } 125 | } else { 126 | parts = append(parts, typeStr) 127 | } 128 | } 129 | return strings.Join(parts, ", ") 130 | } 131 | 132 | // sortMapKeysBySlashCount sorts Structure slices by their path's slash count 133 | func sortMapKeysBySlashCount(inputMap map[string]*Structure) []*Structure { 134 | items := make([]*Structure, 0, len(inputMap)) 135 | for _, v := range inputMap { 136 | items = append(items, v) 137 | } 138 | 139 | sort.Slice(items, func(i, j int) bool { 140 | countI := strings.Count(items[i].Path, "/") 141 | countJ := strings.Count(items[j].Path, "/") 142 | 143 | if countI != countJ { 144 | return countI > countJ 145 | } 146 | 147 | return items[i].Path < items[j].Path 148 | }) 149 | 150 | return items 151 | } 152 | 153 | // maxValue returns the maximum of two integers 154 | func maxValue(a, b int) int { 155 | if a > b { 156 | return a 157 | } 158 | return b 159 | } 160 | 161 | // isValidCustomTypeName checks if a given string is a valid custom type name 162 | func isValidCustomTypeName(s string) bool { 163 | if len(s) == 0 { 164 | return false 165 | } 166 | 167 | // Check if it's a standard type 168 | if stdTypes[s] { 169 | return false 170 | } 171 | 172 | // Check that the first character is a letter (considering Unicode) or underscore 173 | firstChar := rune(s[0]) 174 | if !unicode.IsLetter(firstChar) && firstChar != '_' { 175 | return false 176 | } 177 | 178 | // Check the remaining characters 179 | for _, char := range s[1:] { 180 | if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' { 181 | return false 182 | } 183 | } 184 | 185 | return true 186 | } 187 | 188 | func deepCopy(src *Structure) *Structure { 189 | elem := &Structure{ 190 | Name: src.Name, 191 | Path: src.Path, 192 | Root: src.Root, 193 | RootField: src.RootField, 194 | StructType: src.StructType, 195 | StringType: src.StringType, 196 | IsStructure: src.IsStructure, 197 | Size: src.Size, 198 | Align: src.Align, 199 | Offset: src.Offset, 200 | } 201 | if src.MetaData != nil { 202 | elem.MetaData = &MetaData{ 203 | BeforeSize: src.MetaData.BeforeSize, 204 | AfterSize: src.MetaData.AfterSize, 205 | Data: src.MetaData.Data, 206 | StartPos: src.MetaData.StartPos, 207 | EndPos: src.MetaData.EndPos, 208 | } 209 | } 210 | if src.NestedFields != nil { 211 | newNestedFields := make([]*Structure, 0, len(src.NestedFields)) 212 | for _, item := range src.NestedFields { 213 | newNestedFields = append(newNestedFields, deepCopy(item)) 214 | } 215 | elem.NestedFields = newNestedFields 216 | } 217 | 218 | return elem 219 | } 220 | -------------------------------------------------------------------------------- /cmd/gofield/utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // TestGetTypeString tests the getTypeString function. 10 | // It verifies that the function correctly converts various AST expressions 11 | // to their string representations. 12 | func TestGetTypeString(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | expr ast.Expr 16 | expected string 17 | }{ 18 | { 19 | name: "identifier", 20 | expr: &ast.Ident{Name: "int"}, 21 | expected: "int", 22 | }, 23 | { 24 | name: "pointer", 25 | expr: &ast.StarExpr{X: &ast.Ident{Name: "int"}}, 26 | expected: "*int", 27 | }, 28 | { 29 | name: "slice", 30 | expr: &ast.ArrayType{Elt: &ast.Ident{Name: "int"}}, 31 | expected: "[]int", 32 | }, 33 | { 34 | name: "map", 35 | expr: &ast.MapType{ 36 | Key: &ast.Ident{Name: "string"}, 37 | Value: &ast.Ident{Name: "int"}, 38 | }, 39 | expected: "map[string]int", 40 | }, 41 | } 42 | 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | result := getTypeString(tt.expr) 46 | if result != tt.expected { 47 | t.Errorf("Expected %q, got %q", tt.expected, result) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | // TestGetFuncTypeString tests the getFuncTypeString function. 54 | // It checks if the function correctly generates string representations 55 | // of function types with various parameters and return values. 56 | func TestGetFuncTypeString(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | funcType *ast.FuncType 60 | expected string 61 | }{ 62 | { 63 | name: "no params, no results", 64 | funcType: &ast.FuncType{ 65 | Params: &ast.FieldList{}, 66 | Results: nil, 67 | }, 68 | expected: "func()", 69 | }, 70 | { 71 | name: "with params and results", 72 | funcType: &ast.FuncType{ 73 | Params: &ast.FieldList{ 74 | List: []*ast.Field{ 75 | {Type: &ast.Ident{Name: "int"}}, 76 | {Type: &ast.Ident{Name: "string"}}, 77 | }, 78 | }, 79 | Results: &ast.FieldList{ 80 | List: []*ast.Field{ 81 | {Type: &ast.Ident{Name: "bool"}}, 82 | }, 83 | }, 84 | }, 85 | expected: "func(int, string) bool", 86 | }, 87 | } 88 | 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | result := getFuncTypeString(tt.funcType) 92 | if result != tt.expected { 93 | t.Errorf("Expected %q, got %q", tt.expected, result) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | // TestSortMapKeysBySlashCount tests the sortMapKeysBySlashCount function. 100 | // It verifies that the function correctly sorts Structure slices 101 | // based on the number of slashes in their paths. 102 | func TestSortMapKeysBySlashCount(t *testing.T) { 103 | input := map[string]*Structure{ 104 | "a": {Path: "a"}, 105 | "a/b": {Path: "a/b"}, 106 | "a/b/c": {Path: "a/b/c"}, 107 | "x/y": {Path: "x/y"}, 108 | } 109 | 110 | expected := []*Structure{ 111 | {Path: "a/b/c"}, 112 | {Path: "a/b"}, 113 | {Path: "x/y"}, 114 | {Path: "a"}, 115 | } 116 | 117 | result := sortMapKeysBySlashCount(input) 118 | 119 | if !reflect.DeepEqual(result, expected) { 120 | t.Errorf("Expected %v, got %v", expected, result) 121 | } 122 | } 123 | 124 | // TestMaxValue tests the maxValue function. 125 | // It checks if the function correctly returns the maximum of two integers 126 | // for various input combinations. 127 | func TestMaxValue(t *testing.T) { 128 | tests := []struct { 129 | a, b, expected int 130 | }{ 131 | {1, 2, 2}, 132 | {5, 3, 5}, 133 | {-1, 0, 0}, 134 | {10, 10, 10}, 135 | } 136 | 137 | for _, tt := range tests { 138 | result := maxValue(tt.a, tt.b) 139 | if result != tt.expected { 140 | t.Errorf("maxValue(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) 141 | } 142 | } 143 | } 144 | 145 | // TestIsValidCustomTypeName tests the isValidCustomTypeName function. 146 | // It verifies that the function correctly identifies valid and invalid 147 | // custom type names according to Go naming conventions. 148 | func TestIsValidCustomTypeName(t *testing.T) { 149 | tests := []struct { 150 | name string 151 | typeName string 152 | expected bool 153 | }{ 154 | {"valid custom type", "MyType", true}, 155 | {"valid with underscore", "My_Type", true}, 156 | {"valid with number", "Type2", true}, 157 | {"invalid starts with number", "2Type", false}, 158 | {"invalid contains space", "My Type", false}, 159 | {"invalid standard type", "int", false}, 160 | {"empty string", "", false}, 161 | } 162 | 163 | for _, tt := range tests { 164 | t.Run(tt.name, func(t *testing.T) { 165 | result := isValidCustomTypeName(tt.typeName) 166 | if result != tt.expected { 167 | t.Errorf("isValidCustomTypeName(%q) = %v; want %v", tt.typeName, result, tt.expected) 168 | } 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t34-dev/go-field-alignment/eb453b099283aa313fca38dd1e32254df5f13959/example.png -------------------------------------------------------------------------------- /example/filex.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | // BadStructX is structure 4 | type BadStructX struct { 5 | a bool // 1 byte 6 | x1 struct { 7 | a bool // 1 byte 8 | b bool // 1 byte 9 | c bool // 1 byte 10 | c1 int32 // 1 byte 11 | c2 bool // 1 byte 12 | } 13 | b int32 // 4 bytes 14 | c bool // 1 byte 15 | d int64 // 8 bytes 16 | xx struct { 17 | a bool // 1 byte 18 | b int32 // 4 bytes 19 | c struct { 20 | a bool // 1 byte 21 | b int32 `json:"logo" db:"logo" example:"http://url"` // 4 bytes 22 | c bool // 1 byte 23 | d int64 // 8 bytes 24 | } 25 | d int64 // 8 bytes 26 | } 27 | x bool // 1 byte 28 | } // BIG 29 | -------------------------------------------------------------------------------- /example/ignore.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | type BadStruct struct { 4 | a bool // 1 byte 5 | b int32 // 4 bytes 6 | c bool // 1 byte 7 | d int64 // 8 bytes 8 | } // test 9 | 10 | // Title 11 | type BadStruct2 struct { 12 | a bool // 1 byte 13 | b int32 // 4 bytes 14 | c bool // 1 byte 15 | d int64 // 8 bytes 16 | } 17 | 18 | /* 19 | Long 20 | Text 21 | */ 22 | type BadStruct3 struct { 23 | d int64 // 8 bytes 24 | b int32 // 4 bytes 25 | a bool // 1 byte 26 | c bool // 1 byte 27 | } 28 | type BadStruct4 struct { 29 | b int32 // 4 bytes 30 | d int64 // 8 bytes 31 | a bool // 1 byte 32 | c bool // 1 byte 33 | } 34 | -------------------------------------------------------------------------------- /example/ignore/ignore.go: -------------------------------------------------------------------------------- 1 | package example 2 | -------------------------------------------------------------------------------- /example/ignore/userx.go: -------------------------------------------------------------------------------- 1 | package example 2 | -------------------------------------------------------------------------------- /example/ignore/userx_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | -------------------------------------------------------------------------------- /example/userx.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | type ExampleExp struct { 4 | E struct { 5 | D struct { 6 | B int64 `json:"logo" db:"logo" example:"http://url"` 7 | F int64 8 | D int32 9 | A bool `json:"id" db:"id"` 10 | C bool // text 11 | 12 | E bool 13 | } 14 | B int64 `json:"logo" db:"logo" example:"http://url"` 15 | F int64 // comment 16 | 17 | A bool `json:"id" db:"id"` 18 | C bool // comment2 19 | 20 | E bool 21 | } 22 | B int64 `json:"logo" db:"logo" example:"http://url"` 23 | F int64 24 | D int32 25 | A bool `json:"id" db:"id"` 26 | C bool // comment3 27 | 28 | } 29 | -------------------------------------------------------------------------------- /example/userx_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | -------------------------------------------------------------------------------- /example_ignore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t34-dev/go-field-alignment/eb453b099283aa313fca38dd1e32254df5f13959/example_ignore.png -------------------------------------------------------------------------------- /example_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t34-dev/go-field-alignment/eb453b099283aa313fca38dd1e32254df5f13959/example_view.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/t34-dev/go-field-alignment/v2 2 | 3 | go 1.22.4 4 | 5 | require github.com/t34-dev/go-text-replacer v1.3.4 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/t34-dev/go-text-replacer v1.3.4 h1:RjrwXnPcpd+uow0ck68YQucWXGsSZq/3Qlgh/RI6Bu0= 2 | github.com/t34-dev/go-text-replacer v1.3.4/go.mod h1:u1peglXh8NVnm8DAQuIOiGflm1Fle6U4dwNJHnV9xAc= 3 | -------------------------------------------------------------------------------- /tests/enter/file.go: -------------------------------------------------------------------------------- 1 | package enter 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Problem1 struct{} 9 | type Problem2 struct { 10 | } 11 | type Problem3 struct { 12 | hello, hello2 string 13 | typer bool 14 | time.Time 15 | time.Duration 16 | time.Location 17 | } 18 | 19 | type StructWithGenerics[T any] struct { 20 | F T 21 | } 22 | 23 | type StructWithMoreGenerics[T1, T2 any, T3 comparable] struct { 24 | F1 T1 25 | F2 T2 26 | F3 T3 27 | } 28 | 29 | type ( 30 | S1 struct { 31 | F1 bool 32 | F2 string 33 | } 34 | 35 | S2 struct { 36 | F1, F2 bool 37 | F3 string 38 | F4 StructWithGenerics[int] 39 | F5 StructWithMoreGenerics[int, float64, string] 40 | } 41 | ) 42 | 43 | type STR string 44 | type STRs []string 45 | 46 | // Test comment 47 | type MyTest struct { 48 | a bool // 1 byte 49 | nameX string 50 | Problem1 struct { 51 | I interface{} 52 | S struct{} 53 | } 54 | b bool // 1 byte 55 | App struct { 56 | // LogLevel 57 | LogLevel string `yaml:"log_level" env-default:"info"` // 2 text 58 | Name string `yaml:"name" env-default:"ms-sso"` 59 | IsProduction bool `yaml:"is_production" env:"IS_PRODUCTION" yaml-default:"true"` 60 | TimeToConfirmRegistration time.Duration `yaml:"tim_to_confirm_registration" env-required:"24h"` 61 | } `yaml:"app"` 62 | } /* some text 63 | dsdsd 64 | dsds 65 | */ 66 | 67 | type MyTest2 struct{} 68 | 69 | type MyTest3 struct { 70 | } 71 | 72 | type MyTest4 struct{} // test 73 | 74 | type MyTest5 struct{} // test 75 | 76 | var Name = "dsds" 77 | 78 | func Get() { 79 | f := MyTest{} 80 | f2 := MyTest2{} 81 | f3 := MyTest3{} 82 | f4 := MyTest4{} 83 | f5 := MyTest5{} 84 | fmt.Println(f, f2, f3, f4, f5, Name) 85 | 86 | //name 87 | } 88 | 89 | // Unaligned Structures 90 | type SimpleGenericUnaligned[T any] struct { 91 | Value T 92 | ID int 93 | Name string 94 | } 95 | 96 | type MultiParamUnaligned[T string, U Number] struct { 97 | First T 98 | Second U 99 | IsValid bool 100 | } 101 | 102 | type OuterUnaligned[T any, U comparable] struct { 103 | Data T 104 | Nested Inner[U] 105 | Priority int 106 | } 107 | 108 | type TreeNodeUnaligned[T any] struct { 109 | Value T 110 | Left *TreeNodeUnaligned[T] 111 | Right *TreeNodeUnaligned[T] 112 | Depth int 113 | IsLeaf bool 114 | } 115 | 116 | type SliceContainerUnaligned[T any] struct { 117 | Items []T 118 | TotalCount int 119 | MaxSize int64 120 | IsReadOnly bool 121 | } 122 | 123 | // Aligned Structures 124 | type SimpleGenericAligned[T any] struct { 125 | Value T 126 | ID int 127 | Name string 128 | } 129 | 130 | type MultiParamAligned[T any, U Number] struct { 131 | First T 132 | Second U 133 | IsValid bool 134 | } 135 | 136 | type OuterAligned[T any, U comparable] struct { 137 | Data T 138 | Nested Inner[U] 139 | Priority int 140 | } 141 | 142 | type TreeNodeAligned[T any] struct { 143 | Value T 144 | Left *TreeNodeAligned[T] 145 | Right *TreeNodeAligned[T] 146 | Depth int 147 | IsLeaf bool 148 | } 149 | 150 | type SliceContainerAligned[T any] struct { 151 | Items []T 152 | TotalCount int 153 | MaxSize int64 154 | IsReadOnly bool 155 | } 156 | 157 | // Common types used in the structures above 158 | type Number interface { 159 | int | int32 | int64 | float32 | float64 160 | } 161 | 162 | type Inner[T comparable] struct { 163 | Key T 164 | Value string 165 | } 166 | -------------------------------------------------------------------------------- /tests/out/file.go: -------------------------------------------------------------------------------- 1 | package enter 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Problem1 struct{} 9 | type Problem2 struct{} 10 | type Problem3 struct { 11 | hello string 12 | hello2 string 13 | time.Time 14 | time.Duration 15 | time.Location 16 | typer bool 17 | } 18 | 19 | type StructWithGenerics[T any] struct { 20 | F T 21 | } 22 | 23 | type StructWithMoreGenerics[T1, T2 any, T3 comparable] struct { 24 | F1 T1 25 | F2 T2 26 | F3 T3 27 | } 28 | 29 | type ( 30 | S1 struct { 31 | F2 string 32 | F1 bool 33 | } 34 | 35 | S2 struct { 36 | F3 string 37 | F4 StructWithGenerics[int] 38 | F5 StructWithMoreGenerics[int, float64, string] 39 | F1 bool 40 | F2 bool 41 | } 42 | ) 43 | 44 | type STR string 45 | type STRs []string 46 | 47 | // Test comment 48 | type MyTest struct { 49 | App struct { 50 | // LogLevel 51 | LogLevel string `yaml:"log_level" env-default:"info"` // 2 text 52 | Name string `yaml:"name" env-default:"ms-sso"` 53 | TimeToConfirmRegistration time.Duration `yaml:"tim_to_confirm_registration" env-required:"24h"` 54 | IsProduction bool `yaml:"is_production" env:"IS_PRODUCTION" yaml-default:"true"` 55 | } `yaml:"app"` 56 | nameX string 57 | Problem1 struct { 58 | I interface{} 59 | S struct{} 60 | } 61 | a bool // 1 byte 62 | b bool // 1 byte 63 | } /* some text 64 | dsdsd 65 | dsds 66 | */ 67 | 68 | type MyTest2 struct{} 69 | 70 | type MyTest3 struct{} 71 | 72 | type MyTest4 struct{} // test 73 | 74 | type MyTest5 struct{} // test 75 | 76 | var Name = "dsds" 77 | 78 | func Get() { 79 | f := MyTest{} 80 | f2 := MyTest2{} 81 | f3 := MyTest3{} 82 | f4 := MyTest4{} 83 | f5 := MyTest5{} 84 | fmt.Println(f, f2, f3, f4, f5, Name) 85 | 86 | //name 87 | } 88 | 89 | // Unaligned Structures 90 | type SimpleGenericUnaligned[T any] struct { 91 | Name string 92 | ID int 93 | Value T 94 | } 95 | 96 | type MultiParamUnaligned[T string, U Number] struct { 97 | IsValid bool 98 | First T 99 | Second U 100 | } 101 | 102 | type OuterUnaligned[T any, U comparable] struct { 103 | Nested Inner[U] 104 | Priority int 105 | Data T 106 | } 107 | 108 | type TreeNodeUnaligned[T any] struct { 109 | Left *TreeNodeUnaligned[T] 110 | Right *TreeNodeUnaligned[T] 111 | Depth int 112 | IsLeaf bool 113 | Value T 114 | } 115 | 116 | type SliceContainerUnaligned[T any] struct { 117 | TotalCount int 118 | MaxSize int64 119 | IsReadOnly bool 120 | Items []T 121 | } 122 | 123 | // Aligned Structures 124 | type SimpleGenericAligned[T any] struct { 125 | Name string 126 | ID int 127 | Value T 128 | } 129 | 130 | type MultiParamAligned[T any, U Number] struct { 131 | IsValid bool 132 | First T 133 | Second U 134 | } 135 | 136 | type OuterAligned[T any, U comparable] struct { 137 | Nested Inner[U] 138 | Priority int 139 | Data T 140 | } 141 | 142 | type TreeNodeAligned[T any] struct { 143 | Left *TreeNodeAligned[T] 144 | Right *TreeNodeAligned[T] 145 | Depth int 146 | IsLeaf bool 147 | Value T 148 | } 149 | 150 | type SliceContainerAligned[T any] struct { 151 | TotalCount int 152 | MaxSize int64 153 | IsReadOnly bool 154 | Items []T 155 | } 156 | 157 | // Common types used in the structures above 158 | type Number interface { 159 | int | int32 | int64 | float32 | float64 160 | } 161 | 162 | type Inner[T comparable] struct { 163 | Value string 164 | Key T 165 | } 166 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version represents the current Version of the program 4 | const Version = "2.0.10" 5 | --------------------------------------------------------------------------------