├── .github ├── ISSUE_TEMPLATE │ └── common.md └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gci │ ├── completion.go │ ├── diff.go │ ├── gcicommand.go │ ├── list.go │ ├── print.go │ ├── root.go │ └── write.go ├── go.mod ├── go.sum ├── internal └── generate.go ├── main.go └── pkg ├── config ├── config.go └── config_test.go ├── format └── format.go ├── gci ├── gci.go ├── gci_test.go ├── testdata.go └── testdata │ ├── module │ ├── .gitattributes │ ├── config.yaml │ ├── go.mod │ ├── internal │ │ ├── bar │ │ │ └── lib.go │ │ ├── foo │ │ │ ├── lib.go │ │ │ └── lib.out.go │ │ └── lib.go │ ├── main.go │ └── main.out.go │ ├── module_canonical │ ├── .gitattributes │ ├── cmd │ │ ├── client │ │ │ ├── main.go │ │ │ └── main.out.go │ │ └── server │ │ │ ├── main.go │ │ │ └── main.out.go │ ├── config.yaml │ ├── go.mod │ └── internal │ │ ├── bar │ │ └── lib.go │ │ ├── foo │ │ ├── lib.go │ │ └── lib.out.go │ │ └── lib.go │ └── module_noncanonical │ ├── .gitattributes │ ├── cmd │ ├── client │ │ ├── main.go │ │ └── main.out.go │ └── server │ │ ├── main.go │ │ └── main.out.go │ ├── config.yaml │ ├── go.mod │ └── internal │ ├── bar │ └── lib.go │ ├── foo │ ├── lib.go │ └── lib.out.go │ └── lib.go ├── io ├── file.go ├── search.go └── stdin.go ├── log └── log.go ├── parse └── parse.go ├── section ├── alias.go ├── blank.go ├── commentline.go ├── commentline_test.go ├── default.go ├── default_test.go ├── dot.go ├── errors.go ├── errors_test.go ├── local_module.go ├── local_module_test.go ├── newline.go ├── newline_test.go ├── parser.go ├── parser_test.go ├── prefix.go ├── prefix_test.go ├── section.go ├── section_test.go ├── standard.go ├── standard_list.go └── standard_test.go ├── specificity ├── default.go ├── local_module.go ├── match.go ├── mismatch.go ├── name.go ├── specificity.go ├── specificity_test.go └── standard.go └── utils └── constants.go /.github/ISSUE_TEMPLATE/common.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bugs or Feature Requests 3 | about: Bugs or Feature Requests 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What version of GCI are you using? 11 | 12 |
13 | 
14 | 
15 | 16 | ### Reproduce Steps 17 | 18 |
19 | 
20 | 
21 | 22 | 23 | ### What did you expect to see? 24 | 25 | 26 | 27 | ### What did you see instead? 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | name: Build and Tests 9 | runs-on: ${{ matrix.os }} 10 | env: 11 | GOPROXY: https://proxy.golang.org 12 | ARCHIVE_OUTDIR: dist/archives 13 | TEST_OUTPUT_FILE_PREFIX: test_report 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macos-latest] 17 | go: [ oldstable, stable ] 18 | steps: 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v4 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go }} 25 | - name: Run make test 26 | env: 27 | COVERAGE_OPTS: "-coverprofile=coverage.txt -covermode=atomic" 28 | run: make test 29 | - name: Run make build 30 | run: make build 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | env: 10 | GOVER: oldstable 11 | GOPROXY: https://proxy.golang.org 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v4 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ${{ env.GOVER }} 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v6 21 | with: 22 | version: ${{ env.GOLANGCILINT_VER }} 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .gitcookies 3 | .idea/ 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # default concurrency is a available CPU number 4 | concurrency: 4 5 | 6 | # timeout for analysis, e.g. 30s, 5m, default is 1m 7 | timeout: 10m 8 | 9 | # exit code when at least one issue was found, default is 1 10 | issues-exit-code: 1 11 | 12 | # include test files or not, default is true 13 | tests: true 14 | 15 | 16 | # output configuration options 17 | output: 18 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" 19 | formats: 20 | - format: tab 21 | 22 | # print lines of code with issue, default is true 23 | print-issued-lines: true 24 | 25 | # print linter name in the end of issue text, default is true 26 | print-linter-name: true 27 | 28 | 29 | # all available settings of specific linters 30 | linters-settings: 31 | gci: 32 | # Checks that no inline Comments are present. 33 | # Default: false 34 | no-inline-comments: false 35 | 36 | # Checks that no prefix Comments(comment lines above an import) are present. 37 | # Default: false 38 | no-prefix-comments: false 39 | 40 | # Section configuration to compare against. 41 | # Section names are case-insensitive and may contain parameters in (). 42 | # Default: ["standard", "default"] 43 | sections: 44 | - standard # Captures all standard packages if they do not match another section. 45 | - default # Contains all imports that could not be matched to another section type. 46 | - prefix(github.com/daixiang0/gci) # Groups all imports with the specified Prefix. 47 | 48 | gofmt: 49 | # simplify code: gofmt with `-s` option, true by default 50 | simplify: true 51 | goimports: 52 | # put imports beginning with prefix after 3rd-party packages; 53 | # it's a comma-separated list of prefixes 54 | local-prefixes: github.com/daixiang0/gci 55 | 56 | linters: 57 | disable-all: true 58 | enable: 59 | - gofmt 60 | - gofumpt 61 | - goimports 62 | - gci 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Xiang Dai 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean generate test build 2 | 3 | BIN_OUTPUT := $(if $(filter $(shell go env GOOS), windows), dist/gci.exe, dist/gci) 4 | 5 | default: clean generate test build 6 | 7 | clean: 8 | @echo BIN_OUTPUT: ${BIN_OUTPUT} 9 | @rm -rf dist cover.out 10 | 11 | build: clean 12 | @go build -v -trimpath -o ${BIN_OUTPUT} . 13 | 14 | test: clean 15 | @go test -v -count=1 -cover ./... 16 | 17 | generate: 18 | @GOEXPERIMENT=arenas,boringcrypto,synctest go generate ./... 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GCI 2 | 3 | GCI, a tool that controls Go package import order and makes it always deterministic. 4 | 5 | The desired output format is highly configurable and allows for more custom formatting than `goimport` does. 6 | 7 | GCI considers a import block based on AST as below: 8 | 9 | ``` 10 | Doc 11 | Name Path Comment 12 | ``` 13 | 14 | All comments will keep as they were, except the isolated comment blocks. 15 | 16 | The isolated comment blocks like below: 17 | 18 | ``` 19 | import ( 20 | "fmt" 21 | // this line is isolated comment 22 | 23 | // those lines belong to one 24 | // isolated comment blocks 25 | 26 | "github.com/daixiang0/gci" 27 | ) 28 | ``` 29 | 30 | GCI splits all import blocks into different sections, now support six section type: 31 | 32 | - standard: Go official imports, like "fmt" 33 | - custom: Custom section, use full and the longest match (match full string first, if multiple matches, use the longest one) 34 | - default: All rest import blocks 35 | - blank: Put blank imports together in a separate group 36 | - dot: Put dot imports together in a separate group 37 | - alias: Put alias imports together in a separate group 38 | - localmodule: Put imports from local packages in a separate group 39 | 40 | The priority is standard > default > custom > blank > dot > alias > localmodule, all sections sort alphabetically inside. 41 | By default, blank, dot, and alias sections are not used, and the corresponding lines end up in the other groups. 42 | 43 | All import blocks use one TAB(`\t`) as Indent. 44 | 45 | Since v0.9.0, GCI always puts C import block as the first. 46 | 47 | **Note**: 48 | 49 | `nolint` is hard to handle at section level, GCI will consider it as a single comment. 50 | 51 | ### LocalModule 52 | 53 | Local module detection is done via reading the module name from the `go.mod` 54 | file in *the directory where `gci` is invoked*. This means: 55 | 56 | - This mode works when `gci` is invoked from a module root (i.e. directory 57 | containing `go.mod`) 58 | - This mode doesn't work with a multi-module setup, i.e. when `gci` is invoked 59 | from a directory containing `go.work` (though it would work if invoked from 60 | within one of the modules in the workspace) 61 | 62 | ## Installation 63 | 64 | To download and install the highest available release version - 65 | 66 | ```shell 67 | go install github.com/daixiang0/gci@latest 68 | ``` 69 | 70 | You may also specify a specific version, for example: 71 | 72 | ```shell 73 | go install github.com/daixiang0/gci@v0.11.2 74 | ``` 75 | 76 | ## Usage 77 | 78 | Now GCI provides two command line methods, mainly for backward compatibility. 79 | 80 | ### New style 81 | 82 | GCI supports three modes of operation 83 | 84 | > **Note** 85 | > 86 | > Since v0.10.0, the `-s` and `--section` flag can only be used multiple times to specify multiple sections. 87 | > For example, you could use `-s standard,default` before, but now you must use `-s standard -s default`. 88 | > This breaking change makes it possible for the project to support specifying multiple custom prefixes. (Please see below.) 89 | 90 | ```shell 91 | $ gci print -h 92 | Print outputs the formatted file. If you want to apply the changes to a file use write instead! 93 | 94 | Usage: 95 | gci print path... [flags] 96 | 97 | Aliases: 98 | print, output 99 | 100 | Flags: 101 | --custom-order Enable custom order of sections 102 | -d, --debug Enables debug output from the formatter 103 | -h, --help help for print 104 | -s, --section stringArray Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). The section order is standard > default > custom > blank > dot > alias > localmodule. The default value is [standard,default]. 105 | standard - standard section that Go provides officially, like "fmt" 106 | Prefix(github.com/daixiang0) - custom section, groups all imports with the specified Prefix. Imports will be matched to the longest Prefix. Multiple custom prefixes may be provided, they will be rendered as distinct sections separated by newline. You can regroup multiple prefixes by separating them with comma: Prefix(github.com/daixiang0,gitlab.com/daixiang0,daixiang0) 107 | default - default section, contains all rest imports 108 | blank - blank section, contains all blank imports. 109 | dot - dot section, contains all dot imports. (default [standard,default]) 110 | alias - alias section, contains all alias imports. 111 | localmodule: localmodule section, contains all imports from local packages 112 | --skip-generated Skip generated files 113 | --skip-vendor Skip files inside vendor directory 114 | ``` 115 | 116 | ```shell 117 | $ gci write -h 118 | Write modifies the specified files in-place 119 | 120 | Usage: 121 | gci write path... [flags] 122 | 123 | Aliases: 124 | write, overwrite 125 | 126 | Flags: 127 | --custom-order Enable custom order of sections 128 | -d, --debug Enables debug output from the formatter 129 | -h, --help help for write 130 | -s, --section stringArray Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). The section order is standard > default > custom > blank > dot > alias > localmodule. The default value is [standard,default]. 131 | standard - standard section that Go provides officially, like "fmt" 132 | Prefix(github.com/daixiang0) - custom section, groups all imports with the specified Prefix. Imports will be matched to the longest Prefix. Multiple custom prefixes may be provided, they will be rendered as distinct sections separated by newline. You can regroup multiple prefixes by separating them with comma: Prefix(github.com/daixiang0,gitlab.com/daixiang0,daixiang0) 133 | default - default section, contains all rest imports 134 | blank - blank section, contains all blank imports. 135 | dot - dot section, contains all dot imports. (default [standard,default]) 136 | alias - alias section, contains all alias imports. 137 | localmodule: localmodule section, contains all imports from local packages 138 | --skip-generated Skip generated files 139 | --skip-vendor Skip files inside vendor directory 140 | ``` 141 | 142 | ```shell 143 | $ gci list -h 144 | Prints the filenames that need to be formatted. If you want to show the diff use diff instead, and if you want to apply the changes use write instead 145 | 146 | Usage: 147 | gci list path... [flags] 148 | 149 | Flags: 150 | --custom-order Enable custom order of sections 151 | -d, --debug Enables debug output from the formatter 152 | -h, --help help for list 153 | -s, --section stringArray Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). The section order is standard > default > custom > blank > dot > alias > localmodule. The default value is [standard,default]. 154 | standard - standard section that Go provides officially, like "fmt" 155 | Prefix(github.com/daixiang0) - custom section, groups all imports with the specified Prefix. Imports will be matched to the longest Prefix. Multiple custom prefixes may be provided, they will be rendered as distinct sections separated by newline. You can regroup multiple prefixes by separating them with comma: Prefix(github.com/daixiang0,gitlab.com/daixiang0,daixiang0) 156 | default - default section, contains all rest imports 157 | blank - blank section, contains all blank imports. 158 | dot - dot section, contains all dot imports. (default [standard,default]) 159 | alias - alias section, contains all alias imports. 160 | localmodule: localmodule section, contains all imports from local packages 161 | --skip-generated Skip generated files 162 | --skip-vendor Skip files inside vendor directory 163 | ``` 164 | 165 | ```shell 166 | $ gci diff -h 167 | Diff prints a patch in the style of the diff tool that contains the required changes to the file to make it adhere to the specified formatting. 168 | 169 | Usage: 170 | gci diff path... [flags] 171 | 172 | Flags: 173 | --custom-order Enable custom order of sections 174 | -d, --debug Enables debug output from the formatter 175 | -h, --help help for diff 176 | -s, --section stringArray Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). The section order is standard > default > custom > blank > dot > alias > localmodule. The default value is [standard,default]. 177 | standard - standard section that Go provides officially, like "fmt" 178 | Prefix(github.com/daixiang0) - custom section, groups all imports with the specified Prefix. Imports will be matched to the longest Prefix. Multiple custom prefixes may be provided, they will be rendered as distinct sections separated by newline. You can regroup multiple prefixes by separating them with comma: Prefix(github.com/daixiang0,gitlab.com/daixiang0,daixiang0) 179 | default - default section, contains all rest imports 180 | blank - blank section, contains all blank imports. 181 | dot - dot section, contains all dot imports. (default [standard,default]) 182 | alias - alias section, contains all alias imports. 183 | localmodule: localmodule section, contains all imports from local packages 184 | --skip-generated Skip generated files 185 | --skip-vendor Skip files inside vendor directory 186 | ``` 187 | 188 | ### Old style 189 | 190 | ```shell 191 | Gci enables automatic formatting of imports in a deterministic manner 192 | If you want to integrate this as part of your CI take a look at golangci-lint. 193 | 194 | Usage: 195 | gci [-diff | -write] [--local localPackageURLs] path... [flags] 196 | gci [command] 197 | 198 | Available Commands: 199 | completion Generate the autocompletion script for the specified shell 200 | diff Prints a git style diff to STDOUT 201 | help Help about any command 202 | list Prints filenames that need to be formatted to STDOUT 203 | print Outputs the formatted file to STDOUT 204 | write Formats the specified files in-place 205 | 206 | Flags: 207 | -d, --diff display diffs instead of rewriting files 208 | -h, --help help for gci 209 | -l, --local strings put imports beginning with this string after 3rd-party packages, separate imports by comma 210 | -v, --version version for gci 211 | -w, --write write result to (source) file instead of stdout 212 | 213 | Use "gci [command] --help" for more information about a command. 214 | ``` 215 | 216 | **Note**:: 217 | 218 | The old style is only for local tests, will be deprecated, please uses new style, `golangci-lint` uses new style as well. 219 | 220 | ## Examples 221 | 222 | Run `gci write -s standard -s default -s "prefix(github.com/daixiang0/gci)" main.go` and you will handle following cases: 223 | 224 | ### simple case 225 | 226 | ```go 227 | package main 228 | import ( 229 | "golang.org/x/tools" 230 | 231 | "fmt" 232 | 233 | "github.com/daixiang0/gci" 234 | ) 235 | ``` 236 | 237 | to 238 | 239 | ```go 240 | package main 241 | import ( 242 | "fmt" 243 | 244 | "golang.org/x/tools" 245 | 246 | "github.com/daixiang0/gci" 247 | ) 248 | ``` 249 | 250 | ### with alias 251 | 252 | ```go 253 | package main 254 | import ( 255 | "fmt" 256 | go "github.com/golang" 257 | "github.com/daixiang0/gci" 258 | ) 259 | ``` 260 | 261 | to 262 | 263 | ```go 264 | package main 265 | import ( 266 | "fmt" 267 | 268 | go "github.com/golang" 269 | 270 | "github.com/daixiang0/gci" 271 | ) 272 | ``` 273 | 274 | ### with blank and dot grouping enabled 275 | 276 | ```go 277 | package main 278 | import ( 279 | "fmt" 280 | go "github.com/golang" 281 | _ "github.com/golang/blank" 282 | . "github.com/golang/dot" 283 | "github.com/daixiang0/gci" 284 | _ "github.com/daixiang0/gci/blank" 285 | . "github.com/daixiang0/gci/dot" 286 | ) 287 | ``` 288 | 289 | to 290 | 291 | ```go 292 | package main 293 | import ( 294 | "fmt" 295 | 296 | go "github.com/golang" 297 | 298 | "github.com/daixiang0/gci" 299 | 300 | _ "github.com/daixiang0/gci/blank" 301 | _ "github.com/golang/blank" 302 | 303 | . "github.com/daixiang0/gci/dot" 304 | . "github.com/golang/dot" 305 | ) 306 | ``` 307 | 308 | ### with alias grouping enabled 309 | 310 | ```go 311 | package main 312 | 313 | import ( 314 | testing "github.com/daixiang0/test" 315 | "fmt" 316 | 317 | g "github.com/golang" 318 | 319 | "github.com/daixiang0/gci" 320 | "github.com/daixiang0/gci/subtest" 321 | ) 322 | ``` 323 | 324 | to 325 | 326 | ```go 327 | package main 328 | 329 | import ( 330 | "fmt" 331 | 332 | "github.com/daixiang0/gci" 333 | "github.com/daixiang0/gci/subtest" 334 | 335 | testing "github.com/daixiang0/test" 336 | g "github.com/golang" 337 | ) 338 | ``` 339 | 340 | ### with localmodule grouping enabled 341 | 342 | Assuming this is run on the root of this repo (i.e. where 343 | `github.com/daixiang0/gci` is a local module) 344 | 345 | ```go 346 | package main 347 | 348 | import ( 349 | "os" 350 | "github.com/daixiang0/gci/cmd/gci" 351 | ) 352 | ``` 353 | 354 | to 355 | 356 | ```go 357 | package main 358 | 359 | import ( 360 | "os" 361 | 362 | "github.com/daixiang0/gci/cmd/gci" 363 | ) 364 | ``` 365 | 366 | ## TODO 367 | 368 | - Ensure only one blank between `Name` and `Path` in an import block 369 | - Ensure only one blank between `Path` and `Comment` in an import block 370 | - Format comments 371 | - Add more testcases 372 | - Support imports completion (please use `goimports` first then use GCI) 373 | - Optimize comments 374 | - Remove Analyzer layer and fully use analyzer syntax 375 | -------------------------------------------------------------------------------- /cmd/gci/completion.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func subCommandOrGoFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 10 | var commandAliases []string 11 | for _, subCmd := range cmd.Commands() { 12 | commandAliases = append(commandAliases, subCmd.Name()) 13 | commandAliases = append(commandAliases, subCmd.Aliases...) 14 | } 15 | for _, subCmdStr := range commandAliases { 16 | if strings.HasPrefix(subCmdStr, toComplete) { 17 | // completion for commands is already provided by cobra 18 | // do not return file completion 19 | return []string{}, cobra.ShellCompDirectiveNoFileComp 20 | } 21 | } 22 | return goFileCompletion(cmd, args, toComplete) 23 | } 24 | 25 | func goFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 26 | return []string{"go"}, cobra.ShellCompDirectiveFilterFileExt 27 | } 28 | -------------------------------------------------------------------------------- /cmd/gci/diff.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/gci" 5 | ) 6 | 7 | // diffCmd represents the diff command 8 | func (e *Executor) initDiff() { 9 | e.newGciCommand( 10 | "diff path...", 11 | "Prints a git style diff to STDOUT", 12 | "Diff prints a patch in the style of the diff tool that contains the required changes to the file to make it adhere to the specified formatting.", 13 | []string{}, 14 | true, 15 | gci.DiffFormattedFiles) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/gci/gcicommand.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "go.uber.org/zap/zapcore" 6 | 7 | "github.com/daixiang0/gci/pkg/config" 8 | "github.com/daixiang0/gci/pkg/log" 9 | "github.com/daixiang0/gci/pkg/section" 10 | ) 11 | 12 | type processingFunc = func(args []string, gciCfg config.Config) error 13 | 14 | func (e *Executor) newGciCommand(use, short, long string, aliases []string, stdInSupport bool, processingFunc processingFunc) *cobra.Command { 15 | var noInlineComments, noPrefixComments, skipGenerated, skipVendor, customOrder, noLexOrder, debug *bool 16 | var sectionStrings, sectionSeparatorStrings *[]string 17 | cmd := cobra.Command{ 18 | Use: use, 19 | Aliases: aliases, 20 | Short: short, 21 | Long: long, 22 | ValidArgsFunction: goFileCompletion, 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | fmtCfg := config.BoolConfig{ 25 | NoInlineComments: *noInlineComments, 26 | NoPrefixComments: *noPrefixComments, 27 | Debug: *debug, 28 | SkipGenerated: *skipGenerated, 29 | SkipVendor: *skipVendor, 30 | CustomOrder: *customOrder, 31 | NoLexOrder: *noLexOrder, 32 | } 33 | gciCfg, err := config.YamlConfig{Cfg: fmtCfg, SectionStrings: *sectionStrings, SectionSeparatorStrings: *sectionSeparatorStrings}.Parse() 34 | if err != nil { 35 | return err 36 | } 37 | if *debug { 38 | log.SetLevel(zapcore.DebugLevel) 39 | } 40 | return processingFunc(args, *gciCfg) 41 | }, 42 | } 43 | if !stdInSupport { 44 | cmd.Args = cobra.MinimumNArgs(1) 45 | } 46 | 47 | // register command as subcommand 48 | e.rootCmd.AddCommand(&cmd) 49 | 50 | debug = cmd.Flags().BoolP("debug", "d", false, "Enables debug output from the formatter") 51 | 52 | sectionHelp := `Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). The section order is standard > default > custom > blank > dot > alias > localmodule. The default value is [standard,default]. 53 | standard - standard section that Go provides officially, like "fmt" 54 | Prefix(github.com/daixiang0) - custom section, groups all imports with the specified Prefix. Imports will be matched to the longest Prefix. Multiple custom prefixes may be provided, they will be rendered as distinct sections separated by newline. You can regroup multiple prefixes by separating them with comma: Prefix(github.com/daixiang0,gitlab.com/daixiang0,daixiang0) 55 | default - default section, contains all rest imports 56 | blank - blank section, contains all blank imports. 57 | dot - dot section, contains all dot imports. 58 | alias - alias section, contains all alias imports. 59 | localmodule: localmodule section, contains all imports from local packages` 60 | 61 | skipGenerated = cmd.Flags().Bool("skip-generated", false, "Skip generated files") 62 | skipVendor = cmd.Flags().Bool("skip-vendor", false, "Skip files inside vendor directory") 63 | 64 | customOrder = cmd.Flags().Bool("custom-order", false, "Enable custom order of sections") 65 | noLexOrder = cmd.Flags().Bool("no-lex-order", false, "Drops lexical ordering for custom sections") 66 | sectionStrings = cmd.Flags().StringArrayP("section", "s", section.DefaultSections().String(), sectionHelp) 67 | 68 | // deprecated 69 | noInlineComments = cmd.Flags().Bool("NoInlineComments", false, "Drops inline comments while formatting") 70 | cmd.Flags().MarkDeprecated("NoInlineComments", "Drops inline comments while formatting") 71 | noPrefixComments = cmd.Flags().Bool("NoPrefixComments", false, "Drops comment lines above an import statement while formatting") 72 | cmd.Flags().MarkDeprecated("NoPrefixComments", "Drops inline comments while formatting") 73 | sectionSeparatorStrings = cmd.Flags().StringSliceP("SectionSeparator", "x", section.DefaultSectionSeparators().String(), "SectionSeparators are inserted between Sections") 74 | cmd.Flags().MarkDeprecated("SectionSeparator", "Drops inline comments while formatting") 75 | cmd.Flags().MarkDeprecated("x", "Drops inline comments while formatting") 76 | 77 | return &cmd 78 | } 79 | -------------------------------------------------------------------------------- /cmd/gci/list.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/gci" 5 | ) 6 | 7 | // listCmd represents the list command 8 | func (e *Executor) initList() { 9 | e.newGciCommand( 10 | "list path...", 11 | "Prints filenames that need to be formatted to STDOUT", 12 | "Prints the filenames that need to be formatted. If you want to show the diff use diff instead, and if you want to apply the changes use write instead", 13 | []string{}, 14 | false, 15 | gci.ListUnFormattedFiles) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/gci/print.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/gci" 5 | ) 6 | 7 | // printCmd represents the print command 8 | func (e *Executor) initPrint() { 9 | e.newGciCommand( 10 | "print path...", 11 | "Outputs the formatted file to STDOUT", 12 | "Print outputs the formatted file. If you want to apply the changes to a file use write instead!", 13 | []string{"output"}, 14 | true, 15 | gci.PrintFormattedFiles) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/gci/root.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/daixiang0/gci/pkg/config" 10 | "github.com/daixiang0/gci/pkg/gci" 11 | "github.com/daixiang0/gci/pkg/log" 12 | "github.com/daixiang0/gci/pkg/section" 13 | ) 14 | 15 | type Executor struct { 16 | rootCmd *cobra.Command 17 | diffMode *bool 18 | writeMode *bool 19 | localFlags *[]string 20 | } 21 | 22 | func NewExecutor(version string) *Executor { 23 | log.InitLogger() 24 | defer log.L().Sync() 25 | 26 | e := Executor{} 27 | rootCmd := cobra.Command{ 28 | Use: "gci [-diff | -write] [--local localPackageURLs] path...", 29 | Short: "Gci controls golang package import order and makes it always deterministic", 30 | Long: "Gci enables automatic formatting of imports in a deterministic manner" + 31 | "\n" + 32 | "If you want to integrate this as part of your CI take a look at golangci-lint.", 33 | ValidArgsFunction: subCommandOrGoFileCompletion, 34 | Args: cobra.MinimumNArgs(1), 35 | Version: version, 36 | RunE: e.runInCompatibilityMode, 37 | } 38 | e.rootCmd = &rootCmd 39 | e.diffMode = rootCmd.Flags().BoolP("diff", "d", false, "display diffs instead of rewriting files") 40 | e.writeMode = rootCmd.Flags().BoolP("write", "w", false, "write result to (source) file instead of stdout") 41 | e.localFlags = rootCmd.Flags().StringSliceP("local", "l", []string{}, "put imports beginning with this string after 3rd-party packages, separate imports by comma") 42 | e.initDiff() 43 | e.initPrint() 44 | e.initWrite() 45 | e.initList() 46 | return &e 47 | } 48 | 49 | // Execute adds all child commands to the root command and sets flags appropriately. 50 | // This is called by main.main(). It only needs to happen once to the rootCmd. 51 | func (e *Executor) Execute() error { 52 | return e.rootCmd.Execute() 53 | } 54 | 55 | func (e *Executor) runInCompatibilityMode(cmd *cobra.Command, args []string) error { 56 | // Workaround since the Deprecation message in Cobra can not be printed to STDERR 57 | _, _ = fmt.Fprintln(os.Stderr, "Using the old parameters is deprecated, please use the named subcommands!") 58 | 59 | if *e.writeMode && *e.diffMode { 60 | return fmt.Errorf("diff and write must not be specified at the same time") 61 | } 62 | // generate section specification from old localFlags format 63 | sections := gci.LocalFlagsToSections(*e.localFlags) 64 | sectionSeparators := section.DefaultSectionSeparators() 65 | cfg := config.Config{ 66 | BoolConfig: config.BoolConfig{ 67 | NoInlineComments: false, 68 | NoPrefixComments: false, 69 | Debug: false, 70 | SkipGenerated: false, 71 | SkipVendor: false, 72 | }, 73 | Sections: sections, 74 | SectionSeparators: sectionSeparators, 75 | } 76 | if *e.writeMode { 77 | return gci.WriteFormattedFiles(args, cfg) 78 | } 79 | if *e.diffMode { 80 | return gci.DiffFormattedFiles(args, cfg) 81 | } 82 | return gci.PrintFormattedFiles(args, cfg) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/gci/write.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/gci" 5 | ) 6 | 7 | // writeCmd represents the write command 8 | func (e *Executor) initWrite() { 9 | e.newGciCommand( 10 | "write path...", 11 | "Formats the specified files in-place", 12 | "Write modifies the specified files in-place", 13 | []string{"overwrite"}, 14 | false, 15 | gci.WriteFormattedFiles) 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/daixiang0/gci 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/hexops/gotextdiff v1.0.3 7 | github.com/pmezard/go-difflib v1.0.0 8 | github.com/spf13/cobra v1.6.1 9 | github.com/stretchr/testify v1.9.0 10 | go.uber.org/zap v1.24.0 11 | golang.org/x/mod v0.20.0 12 | golang.org/x/sync v0.8.0 13 | golang.org/x/tools v0.24.0 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 20 | github.com/spf13/pflag v1.0.5 // indirect 21 | go.uber.org/atomic v1.7.0 // indirect 22 | go.uber.org/multierr v1.6.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 8 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 9 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 10 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 11 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 12 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 16 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 17 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 18 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 19 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 25 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 26 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 27 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 28 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 29 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 30 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 31 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 32 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 33 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 34 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 35 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 36 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 37 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 38 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 39 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /internal/generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "go/format" 8 | "os" 9 | "runtime" 10 | "slices" 11 | "strings" 12 | "sync" 13 | "text/template" 14 | 15 | "golang.org/x/sync/errgroup" 16 | "golang.org/x/tools/go/packages" 17 | ) 18 | 19 | //go:generate go run . 20 | 21 | const outputFile = "../pkg/section/standard_list.go" 22 | 23 | const stdTemplate = ` 24 | package section 25 | 26 | // Code generated based on {{ .Version }}. DO NOT EDIT. 27 | 28 | var standardPackages = map[string]struct{}{ 29 | {{- range $pkg := .Packages }} 30 | "{{ $pkg }}": {}, 31 | {{- end}} 32 | } 33 | 34 | ` 35 | 36 | func main() { 37 | err := generate() 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | 43 | // update from https://go.dev/doc/install/source#environment 44 | var list = `aix ppc64 45 | android 386 46 | android amd64 47 | android arm 48 | android arm64 49 | darwin amd64 50 | darwin arm64 51 | dragonfly amd64 52 | freebsd 386 53 | freebsd amd64 54 | freebsd arm 55 | illumos amd64 56 | ios arm64 57 | js wasm 58 | linux 386 59 | linux amd64 60 | linux arm 61 | linux arm64 62 | linux loong64 63 | linux mips 64 | linux mipsle 65 | linux mips64 66 | linux mips64le 67 | linux ppc64 68 | linux ppc64le 69 | linux riscv64 70 | linux s390x 71 | netbsd 386 72 | netbsd amd64 73 | netbsd arm 74 | openbsd 386 75 | openbsd amd64 76 | openbsd arm 77 | openbsd arm64 78 | plan9 386 79 | plan9 amd64 80 | plan9 arm 81 | solaris amd64 82 | wasip1 wasm 83 | windows 386 84 | windows amd64 85 | windows arm 86 | windows arm64` 87 | 88 | func generate() error { 89 | var all []*packages.Package 90 | 91 | writeLock := sync.Mutex{} 92 | 93 | g, _ := errgroup.WithContext(context.Background()) 94 | for _, pair := range strings.Split(list, "\n") { 95 | pair := pair 96 | g.Go(func() error { 97 | goos, goarch, found := strings.Cut(pair, "\t") 98 | if !found { 99 | return nil 100 | } 101 | 102 | pkgs, err := packages.Load(&packages.Config{ 103 | Mode: packages.NeedName, 104 | Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch, "GOEXPERIMENT=arenas,boringcrypto"), 105 | }, "std") 106 | if err != nil { 107 | return err 108 | } 109 | fmt.Println("loaded", goos, goarch, len(pkgs)) 110 | 111 | writeLock.Lock() 112 | defer writeLock.Unlock() 113 | 114 | all = append(all, pkgs...) 115 | return nil 116 | }) 117 | } 118 | 119 | if err := g.Wait(); err != nil { 120 | return err 121 | } 122 | 123 | uniquePkgs := make(map[string]struct{}) 124 | 125 | // go list std | grep -v vendor | grep -v internal 126 | for _, pkg := range all { 127 | if !strings.Contains(pkg.PkgPath, "internal") && !strings.Contains(pkg.PkgPath, "vendor") { 128 | uniquePkgs[pkg.PkgPath] = struct{}{} 129 | } 130 | } 131 | 132 | pkgs := make([]string, 0, len(uniquePkgs)) 133 | for pkg := range uniquePkgs { 134 | pkgs = append(pkgs, pkg) 135 | } 136 | 137 | slices.Sort(pkgs) 138 | 139 | file, err := os.Create(outputFile) 140 | if err != nil { 141 | return err 142 | } 143 | defer file.Close() 144 | 145 | models := map[string]interface{}{ 146 | "Packages": pkgs, 147 | "Version": runtime.Version(), 148 | } 149 | 150 | tlt, err := template.New("std-packages").Parse(stdTemplate) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | b := &bytes.Buffer{} 156 | 157 | err = tlt.Execute(b, models) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | // gofmt 163 | source, err := format.Source(b.Bytes()) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | _, err = file.Write(source) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/daixiang0/gci/cmd/gci" 7 | ) 8 | 9 | var version = "0.13.6" 10 | 11 | func main() { 12 | e := gci.NewExecutor(version) 13 | 14 | err := e.Execute() 15 | if err != nil { 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "sort" 5 | 6 | "gopkg.in/yaml.v3" 7 | 8 | "github.com/daixiang0/gci/pkg/section" 9 | ) 10 | 11 | var defaultOrder = map[string]int{ 12 | section.StandardType: 0, 13 | section.DefaultType: 1, 14 | section.CustomType: 2, 15 | section.BlankType: 3, 16 | section.DotType: 4, 17 | section.AliasType: 5, 18 | section.LocalModuleType: 6, 19 | } 20 | 21 | type BoolConfig struct { 22 | NoInlineComments bool `yaml:"no-inlineComments"` 23 | NoPrefixComments bool `yaml:"no-prefixComments"` 24 | Debug bool `yaml:"-"` 25 | SkipGenerated bool `yaml:"skipGenerated"` 26 | SkipVendor bool `yaml:"skipVendor"` 27 | CustomOrder bool `yaml:"customOrder"` 28 | NoLexOrder bool `yaml:"noLexOrder"` 29 | } 30 | 31 | type Config struct { 32 | BoolConfig 33 | Sections section.SectionList 34 | SectionSeparators section.SectionList 35 | } 36 | 37 | type YamlConfig struct { 38 | Cfg BoolConfig `yaml:",inline"` 39 | SectionStrings []string `yaml:"sections"` 40 | SectionSeparatorStrings []string `yaml:"sectionseparators"` 41 | 42 | // Since history issue, Golangci-lint needs Analyzer to run and GCI add an Analyzer layer to integrate. 43 | // The ModPath param is only from analyzer.go, no need to set it in all other places. 44 | ModPath string `yaml:"-"` 45 | } 46 | 47 | func (g YamlConfig) Parse() (*Config, error) { 48 | var err error 49 | 50 | sections, err := section.Parse(g.SectionStrings) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if sections == nil { 55 | sections = section.DefaultSections() 56 | } 57 | if err := configureSections(sections, g.ModPath); err != nil { 58 | return nil, err 59 | } 60 | 61 | // if default order sorted sections 62 | if !g.Cfg.CustomOrder { 63 | sort.Slice(sections, func(i, j int) bool { 64 | sectionI, sectionJ := sections[i].Type(), sections[j].Type() 65 | 66 | if g.Cfg.NoLexOrder || sectionI != sectionJ { 67 | return defaultOrder[sectionI] < defaultOrder[sectionJ] 68 | } 69 | 70 | return sections[i].String() < sections[j].String() 71 | }) 72 | } 73 | 74 | sectionSeparators, err := section.Parse(g.SectionSeparatorStrings) 75 | if err != nil { 76 | return nil, err 77 | } 78 | if sectionSeparators == nil { 79 | sectionSeparators = section.DefaultSectionSeparators() 80 | } 81 | 82 | return &Config{g.Cfg, sections, sectionSeparators}, nil 83 | } 84 | 85 | func ParseConfig(in string) (*Config, error) { 86 | config := YamlConfig{} 87 | 88 | err := yaml.Unmarshal([]byte(in), &config) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | gciCfg, err := config.Parse() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return gciCfg, nil 99 | } 100 | 101 | // configureSections now only do golang module path finding. 102 | // Since history issue, Golangci-lint needs Analyzer to run and GCI add an Analyzer layer to integrate. 103 | // The path param is from analyzer.go, in all other places should pass empty string. 104 | func configureSections(sections section.SectionList, path string) error { 105 | for _, sec := range sections { 106 | switch s := sec.(type) { 107 | case *section.LocalModule: 108 | if err := s.Configure(path); err != nil { 109 | return err 110 | } 111 | } 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/daixiang0/gci/pkg/section" 9 | ) 10 | 11 | // the custom sections sort alphabetically as default. 12 | func TestParseOrder(t *testing.T) { 13 | cfg := YamlConfig{ 14 | SectionStrings: []string{"default", "prefix(github/daixiang0/gci)", "prefix(github/daixiang0/gai)"}, 15 | } 16 | gciCfg, err := cfg.Parse() 17 | assert.NoError(t, err) 18 | assert.Equal(t, section.SectionList{section.Default{}, section.Custom{Prefix: "github/daixiang0/gai"}, section.Custom{Prefix: "github/daixiang0/gci"}}, gciCfg.Sections) 19 | } 20 | 21 | func TestParseCustomOrder(t *testing.T) { 22 | cfg := YamlConfig{ 23 | SectionStrings: []string{"default", "prefix(github/daixiang0/gci)", "prefix(github/daixiang0/gai)"}, 24 | Cfg: BoolConfig{ 25 | CustomOrder: true, 26 | }, 27 | } 28 | gciCfg, err := cfg.Parse() 29 | assert.NoError(t, err) 30 | assert.Equal(t, section.SectionList{section.Default{}, section.Custom{Prefix: "github/daixiang0/gci"}, section.Custom{Prefix: "github/daixiang0/gai"}}, gciCfg.Sections) 31 | } 32 | 33 | func TestParseNoLexOrder(t *testing.T) { 34 | cfg := YamlConfig{ 35 | SectionStrings: []string{"prefix(github/daixiang0/gci)", "prefix(github/daixiang0/gai)", "default"}, 36 | Cfg: BoolConfig{ 37 | NoLexOrder: true, 38 | }, 39 | } 40 | 41 | gciCfg, err := cfg.Parse() 42 | assert.NoError(t, err) 43 | assert.Equal(t, section.SectionList{section.Default{}, section.Custom{Prefix: "github/daixiang0/gci"}, section.Custom{Prefix: "github/daixiang0/gai"}}, gciCfg.Sections) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/format/format.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/daixiang0/gci/pkg/config" 7 | "github.com/daixiang0/gci/pkg/log" 8 | "github.com/daixiang0/gci/pkg/parse" 9 | "github.com/daixiang0/gci/pkg/section" 10 | "github.com/daixiang0/gci/pkg/specificity" 11 | ) 12 | 13 | type Block struct { 14 | Start, End int 15 | } 16 | 17 | type resultMap map[string][]*Block 18 | 19 | func Format(data []*parse.GciImports, cfg *config.Config) (resultMap, error) { 20 | result := make(resultMap, len(cfg.Sections)) 21 | for _, d := range data { 22 | // determine match specificity for every available section 23 | var bestSection section.Section 24 | var bestSectionSpecificity specificity.MatchSpecificity = specificity.MisMatch{} 25 | for _, section := range cfg.Sections { 26 | sectionSpecificity := section.MatchSpecificity(d) 27 | if sectionSpecificity.IsMoreSpecific(specificity.MisMatch{}) && sectionSpecificity.Equal(bestSectionSpecificity) { 28 | // specificity is identical 29 | // return nil, section.EqualSpecificityMatchError{} 30 | return nil, nil 31 | } 32 | if sectionSpecificity.IsMoreSpecific(bestSectionSpecificity) { 33 | // better match found 34 | bestSectionSpecificity = sectionSpecificity 35 | bestSection = section 36 | } 37 | } 38 | if bestSection == nil { 39 | return nil, section.NoMatchingSectionForImportError{Imports: d} 40 | } 41 | log.L().Debug(fmt.Sprintf("Matched import %v to section %s", d, bestSection)) 42 | result[bestSection.String()] = append(result[bestSection.String()], &Block{d.Start, d.End}) 43 | } 44 | 45 | return result, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/gci/gci.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | goFormat "go/format" 8 | "os" 9 | "sync" 10 | 11 | "github.com/hexops/gotextdiff" 12 | "github.com/hexops/gotextdiff/myers" 13 | "github.com/hexops/gotextdiff/span" 14 | "golang.org/x/sync/errgroup" 15 | 16 | "github.com/daixiang0/gci/pkg/config" 17 | "github.com/daixiang0/gci/pkg/format" 18 | "github.com/daixiang0/gci/pkg/io" 19 | "github.com/daixiang0/gci/pkg/log" 20 | "github.com/daixiang0/gci/pkg/parse" 21 | "github.com/daixiang0/gci/pkg/section" 22 | "github.com/daixiang0/gci/pkg/utils" 23 | ) 24 | 25 | func LocalFlagsToSections(localFlags []string) section.SectionList { 26 | sections := section.DefaultSections() 27 | // Add all local arguments as ImportPrefix sections 28 | // for _, l := range localFlags { 29 | // sections = append(sections, section.Section{l, nil, nil}) 30 | // } 31 | return sections 32 | } 33 | 34 | func PrintFormattedFiles(paths []string, cfg config.Config) error { 35 | return processStdInAndGoFilesInPaths(paths, cfg, func(filePath string, unmodifiedFile, formattedFile []byte) error { 36 | fmt.Print(string(formattedFile)) 37 | return nil 38 | }) 39 | } 40 | 41 | func WriteFormattedFiles(paths []string, cfg config.Config) error { 42 | return processGoFilesInPaths(paths, cfg, func(filePath string, unmodifiedFile, formattedFile []byte) error { 43 | if bytes.Equal(unmodifiedFile, formattedFile) { 44 | log.L().Debug(fmt.Sprintf("Skipping correctly formatted File: %s", filePath)) 45 | return nil 46 | } 47 | log.L().Info(fmt.Sprintf("Writing formatted File: %s", filePath)) 48 | return os.WriteFile(filePath, formattedFile, 0o644) 49 | }) 50 | } 51 | 52 | func ListUnFormattedFiles(paths []string, cfg config.Config) error { 53 | return processGoFilesInPaths(paths, cfg, func(filePath string, unmodifiedFile, formattedFile []byte) error { 54 | if bytes.Equal(unmodifiedFile, formattedFile) { 55 | return nil 56 | } 57 | fmt.Println(filePath) 58 | return nil 59 | }) 60 | } 61 | 62 | func DiffFormattedFiles(paths []string, cfg config.Config) error { 63 | return processStdInAndGoFilesInPaths(paths, cfg, func(filePath string, unmodifiedFile, formattedFile []byte) error { 64 | fileURI := span.URIFromPath(filePath) 65 | edits := myers.ComputeEdits(fileURI, string(unmodifiedFile), string(formattedFile)) 66 | unifiedEdits := gotextdiff.ToUnified(filePath, filePath, string(unmodifiedFile), edits) 67 | fmt.Printf("%v", unifiedEdits) 68 | return nil 69 | }) 70 | } 71 | 72 | func DiffFormattedFilesToArray(paths []string, cfg config.Config, diffs *[]string, lock *sync.Mutex) error { 73 | log.InitLogger() 74 | defer log.L().Sync() 75 | return processStdInAndGoFilesInPaths(paths, cfg, func(filePath string, unmodifiedFile, formattedFile []byte) error { 76 | fileURI := span.URIFromPath(filePath) 77 | edits := myers.ComputeEdits(fileURI, string(unmodifiedFile), string(formattedFile)) 78 | unifiedEdits := gotextdiff.ToUnified(filePath, filePath, string(unmodifiedFile), edits) 79 | lock.Lock() 80 | *diffs = append(*diffs, fmt.Sprint(unifiedEdits)) 81 | lock.Unlock() 82 | return nil 83 | }) 84 | } 85 | 86 | type fileFormattingFunc func(filePath string, unmodifiedFile, formattedFile []byte) error 87 | 88 | func processStdInAndGoFilesInPaths(paths []string, cfg config.Config, fileFunc fileFormattingFunc) error { 89 | return ProcessFiles(io.StdInGenerator.Combine(io.GoFilesInPathsGenerator(paths, cfg.SkipVendor)), cfg, fileFunc) 90 | } 91 | 92 | func processGoFilesInPaths(paths []string, cfg config.Config, fileFunc fileFormattingFunc) error { 93 | return ProcessFiles(io.GoFilesInPathsGenerator(paths, cfg.SkipVendor), cfg, fileFunc) 94 | } 95 | 96 | func ProcessFiles(fileGenerator io.FileGeneratorFunc, cfg config.Config, fileFunc fileFormattingFunc) error { 97 | var taskGroup errgroup.Group 98 | files, err := fileGenerator() 99 | if err != nil { 100 | return err 101 | } 102 | for _, file := range files { 103 | // run file processing in parallel 104 | taskGroup.Go(processingFunc(file, cfg, fileFunc)) 105 | } 106 | return taskGroup.Wait() 107 | } 108 | 109 | func processingFunc(file io.FileObj, cfg config.Config, formattingFunc fileFormattingFunc) func() error { 110 | return func() error { 111 | unmodifiedFile, formattedFile, err := LoadFormatGoFile(file, cfg) 112 | if err != nil { 113 | // if errors.Is(err, FileParsingError{}) { 114 | // // do not process files that are improperly formatted 115 | // return nil 116 | // } 117 | return err 118 | } 119 | return formattingFunc(file.Path(), unmodifiedFile, formattedFile) 120 | } 121 | } 122 | 123 | func LoadFormatGoFile(file io.FileObj, cfg config.Config) (src, dist []byte, err error) { 124 | src, err = file.Load() 125 | log.L().Debug(fmt.Sprintf("Loaded File: %s", file.Path())) 126 | if err != nil { 127 | return nil, nil, err 128 | } 129 | 130 | return LoadFormat(src, file.Path(), cfg) 131 | } 132 | 133 | func LoadFormat(in []byte, path string, cfg config.Config) (src, dist []byte, err error) { 134 | src = in 135 | 136 | if cfg.SkipGenerated && parse.IsGeneratedFileByComment(string(src)) { 137 | return src, src, nil 138 | } 139 | 140 | imports, headEnd, tailStart, cStart, cEnd, err := parse.ParseFile(src, path) 141 | if err != nil { 142 | if errors.Is(err, parse.NoImportError{}) { 143 | return src, src, nil 144 | } 145 | return nil, nil, err 146 | } 147 | 148 | // do not do format if only one import 149 | if len(imports) <= 1 { 150 | return src, src, nil 151 | } 152 | 153 | result, err := format.Format(imports, &cfg) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | 158 | firstWithIndex := true 159 | 160 | var body []byte 161 | 162 | // order by section list 163 | for _, s := range cfg.Sections { 164 | if len(result[s.String()]) > 0 { 165 | if len(body) > 0 { 166 | body = append(body, utils.Linebreak) 167 | } 168 | for _, d := range result[s.String()] { 169 | AddIndent(&body, &firstWithIndex) 170 | body = append(body, src[d.Start:d.End]...) 171 | } 172 | } 173 | } 174 | 175 | head := make([]byte, headEnd) 176 | copy(head, src[:headEnd]) 177 | tail := make([]byte, len(src)-tailStart) 178 | copy(tail, src[tailStart:]) 179 | 180 | // ensure C 181 | if cStart != 0 { 182 | head = append(head, src[cStart:cEnd]...) 183 | head = append(head, utils.Linebreak) 184 | } 185 | 186 | // add beginning of import block 187 | head = append(head, `import (`...) 188 | head = append(head, utils.Linebreak) 189 | // add end of import block 190 | body = append(body, []byte{utils.RightParenthesis, utils.Linebreak}...) 191 | 192 | log.L().Debug(fmt.Sprintf("head:\n%s", head)) 193 | log.L().Debug(fmt.Sprintf("body:\n%s", body)) 194 | if len(tail) > 20 { 195 | log.L().Debug(fmt.Sprintf("tail:\n%s", tail[:20])) 196 | } else { 197 | log.L().Debug(fmt.Sprintf("tail:\n%s", tail)) 198 | } 199 | 200 | var totalLen int 201 | slices := [][]byte{head, body, tail} 202 | for _, s := range slices { 203 | totalLen += len(s) 204 | } 205 | dist = make([]byte, totalLen) 206 | var i int 207 | for _, s := range slices { 208 | i += copy(dist[i:], s) 209 | } 210 | 211 | // remove ^M(\r\n) from Win to Unix 212 | dist = bytes.ReplaceAll(dist, []byte{utils.WinLinebreak}, []byte{utils.Linebreak}) 213 | 214 | log.L().Debug(fmt.Sprintf("raw:\n%s", dist)) 215 | dist, err = goFormat.Source(dist) 216 | if err != nil { 217 | return nil, nil, err 218 | } 219 | 220 | return src, dist, nil 221 | } 222 | 223 | func AddIndent(in *[]byte, first *bool) { 224 | if *first { 225 | *first = false 226 | return 227 | } 228 | *in = append(*in, utils.Indent) 229 | } 230 | -------------------------------------------------------------------------------- /pkg/gci/gci_test.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/daixiang0/gci/pkg/config" 14 | "github.com/daixiang0/gci/pkg/io" 15 | "github.com/daixiang0/gci/pkg/log" 16 | ) 17 | 18 | func init() { 19 | log.InitLogger() 20 | defer log.L().Sync() 21 | } 22 | 23 | func TestRun(t *testing.T) { 24 | for i := range testCases { 25 | t.Run(fmt.Sprintf("run case: %s", testCases[i].name), func(t *testing.T) { 26 | config, err := config.ParseConfig(testCases[i].config) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | old, new, err := LoadFormat([]byte(testCases[i].in), "", *config) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | assert.NoError(t, err) 37 | assert.Equal(t, testCases[i].in, string(old)) 38 | assert.Equal(t, testCases[i].out, string(new)) 39 | }) 40 | } 41 | } 42 | 43 | func chdir(t *testing.T, dir string) { 44 | oldWd, err := os.Getwd() 45 | require.NoError(t, err) 46 | require.NoError(t, os.Chdir(dir)) 47 | 48 | // change back at the end of the test 49 | t.Cleanup(func() { os.Chdir(oldWd) }) 50 | } 51 | 52 | func readConfig(t *testing.T, configPath string) *config.Config { 53 | rawConfig, err := os.ReadFile(configPath) 54 | require.NoError(t, err) 55 | cfg, err := config.ParseConfig(string(rawConfig)) 56 | require.NoError(t, err) 57 | 58 | return cfg 59 | } 60 | 61 | func TestRunWithLocalModule(t *testing.T) { 62 | tests := []struct { 63 | name string 64 | moduleDir string 65 | // files with a corresponding '*.out.go' file containing the expected 66 | // result of formatting 67 | testedFiles []string 68 | }{ 69 | { 70 | name: `default module test case`, 71 | moduleDir: filepath.Join("testdata", "module"), 72 | testedFiles: []string{ 73 | "main.go", 74 | filepath.Join("internal", "foo", "lib.go"), 75 | }, 76 | }, 77 | { 78 | name: `canonical module without go sources in root dir`, 79 | moduleDir: filepath.Join("testdata", "module_canonical"), 80 | testedFiles: []string{ 81 | filepath.Join("cmd", "client", "main.go"), 82 | filepath.Join("cmd", "server", "main.go"), 83 | filepath.Join("internal", "foo", "lib.go"), 84 | }, 85 | }, 86 | { 87 | name: `non-canonical module without go sources in root dir`, 88 | moduleDir: filepath.Join("testdata", "module_noncanonical"), 89 | testedFiles: []string{ 90 | filepath.Join("cmd", "client", "main.go"), 91 | filepath.Join("cmd", "server", "main.go"), 92 | filepath.Join("internal", "foo", "lib.go"), 93 | }, 94 | }, 95 | } 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | // run subtests for expected module loading behaviour 99 | chdir(t, tt.moduleDir) 100 | cfg := readConfig(t, "config.yaml") 101 | 102 | for _, path := range tt.testedFiles { 103 | t.Run(path, func(t *testing.T) { 104 | // *.go -> *.out.go 105 | expected, err := os.ReadFile(strings.TrimSuffix(path, ".go") + ".out.go") 106 | require.NoError(t, err) 107 | 108 | _, got, err := LoadFormatGoFile(io.File{path}, *cfg) 109 | 110 | require.NoError(t, err) 111 | require.Equal(t, string(expected), string(got)) 112 | }) 113 | } 114 | }) 115 | } 116 | } 117 | 118 | func TestRunWithLocalModuleWithPackageLoadFailure(t *testing.T) { 119 | // just a directory with no Go modules 120 | dir := t.TempDir() 121 | configContent := "sections:\n - LocalModule\n" 122 | 123 | chdir(t, dir) 124 | _, err := config.ParseConfig(configContent) 125 | require.ErrorContains(t, err, "go.mod: open go.mod:") 126 | } 127 | -------------------------------------------------------------------------------- /pkg/gci/testdata.go: -------------------------------------------------------------------------------- 1 | package gci 2 | 3 | type Cases struct { 4 | name, config, in, out string 5 | } 6 | 7 | var commonConfig = `sections: 8 | - Standard 9 | - Default 10 | - Prefix(github.com/daixiang0) 11 | ` 12 | 13 | var testCases = []Cases{ 14 | { 15 | "already-good", 16 | 17 | commonConfig, 18 | 19 | `package main 20 | 21 | import ( 22 | "fmt" 23 | 24 | g "github.com/golang" 25 | 26 | "github.com/daixiang0/gci" 27 | ) 28 | `, 29 | `package main 30 | 31 | import ( 32 | "fmt" 33 | 34 | g "github.com/golang" 35 | 36 | "github.com/daixiang0/gci" 37 | ) 38 | `, 39 | }, 40 | { 41 | "blank-format", 42 | 43 | commonConfig, 44 | 45 | `package main 46 | import ( 47 | "fmt" 48 | 49 | // comment 50 | g "github.com/golang" // comment 51 | 52 | "github.com/daixiang0/gci" 53 | ) 54 | `, 55 | `package main 56 | 57 | import ( 58 | "fmt" 59 | 60 | // comment 61 | g "github.com/golang" // comment 62 | 63 | "github.com/daixiang0/gci" 64 | ) 65 | `, 66 | }, 67 | { 68 | "cgo-block", 69 | 70 | commonConfig, 71 | 72 | `package main 73 | 74 | import ( 75 | /* 76 | #include "types.h" 77 | */ 78 | "C" 79 | ) 80 | `, 81 | `package main 82 | 83 | import ( 84 | /* 85 | #include "types.h" 86 | */ 87 | "C" 88 | ) 89 | `, 90 | }, 91 | { 92 | "cgo-block-after-import", 93 | 94 | commonConfig, 95 | 96 | `package main 97 | 98 | import ( 99 | "fmt" 100 | 101 | "github.com/daixiang0/gci" 102 | g "github.com/golang" 103 | ) 104 | 105 | // #cgo CFLAGS: -DPNG_DEBUG=1 106 | // #cgo amd64 386 CFLAGS: -DX86=1 107 | // #cgo LDFLAGS: -lpng 108 | // #include 109 | import "C" 110 | `, 111 | `package main 112 | 113 | // #cgo CFLAGS: -DPNG_DEBUG=1 114 | // #cgo amd64 386 CFLAGS: -DX86=1 115 | // #cgo LDFLAGS: -lpng 116 | // #include 117 | import "C" 118 | 119 | import ( 120 | "fmt" 121 | 122 | g "github.com/golang" 123 | 124 | "github.com/daixiang0/gci" 125 | ) 126 | `, 127 | }, 128 | { 129 | "cgo-block-before-import", 130 | 131 | commonConfig, 132 | 133 | `package main 134 | 135 | // #cgo CFLAGS: -DPNG_DEBUG=1 136 | // #cgo amd64 386 CFLAGS: -DX86=1 137 | // #cgo LDFLAGS: -lpng 138 | // #include 139 | import "C" 140 | 141 | import ( 142 | "fmt" 143 | 144 | "github.com/daixiang0/gci" 145 | 146 | g "github.com/golang" 147 | ) 148 | `, 149 | `package main 150 | 151 | // #cgo CFLAGS: -DPNG_DEBUG=1 152 | // #cgo amd64 386 CFLAGS: -DX86=1 153 | // #cgo LDFLAGS: -lpng 154 | // #include 155 | import "C" 156 | 157 | import ( 158 | "fmt" 159 | 160 | g "github.com/golang" 161 | 162 | "github.com/daixiang0/gci" 163 | ) 164 | `, 165 | }, 166 | { 167 | "cgo-block-mixed", 168 | 169 | commonConfig, 170 | 171 | `package main 172 | 173 | import ( 174 | /* #include "types.h" 175 | */"C" 176 | ) 177 | `, 178 | `package main 179 | 180 | import ( 181 | /* #include "types.h" 182 | */"C" 183 | ) 184 | `, 185 | }, 186 | { 187 | "cgo-block-mixed-with-content", 188 | 189 | commonConfig, 190 | 191 | `package main 192 | 193 | import ( 194 | /* #include "types.h" 195 | #include "other.h" */"C" 196 | ) 197 | `, 198 | `package main 199 | 200 | import ( 201 | /* #include "types.h" 202 | #include "other.h" */"C" 203 | ) 204 | `, 205 | }, 206 | { 207 | "cgo-block-prefix", 208 | 209 | commonConfig, 210 | 211 | `package main 212 | 213 | import ( 214 | /* #include "types.h" */ "C" 215 | ) 216 | `, 217 | `package main 218 | 219 | import ( 220 | /* #include "types.h" */ "C" 221 | ) 222 | `, 223 | }, 224 | { 225 | "cgo-block-single-line", 226 | 227 | commonConfig, 228 | 229 | `package main 230 | 231 | import ( 232 | /* #include "types.h" */ 233 | "C" 234 | ) 235 | `, 236 | `package main 237 | 238 | import ( 239 | /* #include "types.h" */ 240 | "C" 241 | ) 242 | `, 243 | }, 244 | { 245 | "cgo-line", 246 | 247 | commonConfig, 248 | 249 | `package main 250 | 251 | import ( 252 | // #include "types.h" 253 | "C" 254 | ) 255 | `, 256 | `package main 257 | 258 | import ( 259 | // #include "types.h" 260 | "C" 261 | ) 262 | `, 263 | }, 264 | { 265 | "cgo-multiline", 266 | 267 | commonConfig, 268 | 269 | `package main 270 | 271 | import ( 272 | // #include "types.h" 273 | // #include "other.h" 274 | "C" 275 | ) 276 | `, 277 | `package main 278 | 279 | import ( 280 | // #include "types.h" 281 | // #include "other.h" 282 | "C" 283 | ) 284 | `, 285 | }, 286 | { 287 | "cgo-single", 288 | 289 | commonConfig, 290 | 291 | `package main 292 | 293 | import ( 294 | "fmt" 295 | 296 | "github.com/daixiang0/gci" 297 | ) 298 | 299 | import "C" 300 | 301 | import "github.com/golang" 302 | 303 | import ( 304 | "github.com/daixiang0/gci" 305 | ) 306 | `, 307 | `package main 308 | 309 | import "C" 310 | 311 | import ( 312 | "fmt" 313 | 314 | "github.com/golang" 315 | 316 | "github.com/daixiang0/gci" 317 | ) 318 | `, 319 | }, 320 | { 321 | "comment", 322 | 323 | commonConfig, 324 | 325 | `package main 326 | import ( 327 | //Do not forget to run Gci 328 | "fmt" 329 | ) 330 | `, 331 | `package main 332 | import ( 333 | //Do not forget to run Gci 334 | "fmt" 335 | ) 336 | `, 337 | }, 338 | { 339 | "comment-before-import", 340 | 341 | commonConfig, 342 | 343 | `package main 344 | 345 | // comment 346 | import ( 347 | "fmt" 348 | "os" 349 | 350 | "github.com/daixiang0/gci" 351 | ) 352 | `, 353 | `package main 354 | 355 | // comment 356 | import ( 357 | "fmt" 358 | "os" 359 | 360 | "github.com/daixiang0/gci" 361 | ) 362 | `, 363 | }, 364 | { 365 | "comment-in-the-tail", 366 | 367 | `sections: 368 | - Standard 369 | - Default 370 | - Prefix(github.com/daixiang0) 371 | `, 372 | `package main 373 | 374 | import ( 375 | "fmt" 376 | 377 | g "github.com/golang" 378 | 379 | "github.com/daixiang0/gci" 380 | ) 381 | 382 | type test int 383 | 384 | // test 385 | `, 386 | `package main 387 | 388 | import ( 389 | "fmt" 390 | 391 | g "github.com/golang" 392 | 393 | "github.com/daixiang0/gci" 394 | ) 395 | 396 | type test int 397 | 398 | // test 399 | `, 400 | }, 401 | { 402 | "comment-top", 403 | 404 | commonConfig, 405 | 406 | `package main 407 | 408 | import ( 409 | "os" // https://pkg.go.dev/os 410 | // https://pkg.go.dev/fmt 411 | "fmt" 412 | ) 413 | `, 414 | `package main 415 | 416 | import ( 417 | // https://pkg.go.dev/fmt 418 | "fmt" 419 | "os" // https://pkg.go.dev/os 420 | ) 421 | `, 422 | }, 423 | { 424 | "comment-without-whitespace", 425 | 426 | commonConfig, 427 | 428 | `package proc 429 | 430 | import ( 431 | "context"// no separating whitespace here //nolint:confusion 432 | ) 433 | `, 434 | `package proc 435 | 436 | import ( 437 | "context"// no separating whitespace here //nolint:confusion 438 | ) 439 | `, 440 | }, 441 | { 442 | "comment-with-slashslash", 443 | 444 | commonConfig, 445 | 446 | `package main 447 | 448 | import ( 449 | "fmt" // https://pkg.go.dev/fmt 450 | ) 451 | `, 452 | `package main 453 | 454 | import ( 455 | "fmt" // https://pkg.go.dev/fmt 456 | ) 457 | `, 458 | }, 459 | { 460 | "custom-order", 461 | 462 | `customOrder: true 463 | sections: 464 | - Prefix(github.com/daixiang0) 465 | - Default 466 | - Standard 467 | `, 468 | `package main 469 | 470 | import ( 471 | "fmt" 472 | 473 | g "github.com/golang" 474 | 475 | "github.com/daixiang0/a" 476 | ) 477 | `, 478 | `package main 479 | 480 | import ( 481 | "github.com/daixiang0/a" 482 | 483 | g "github.com/golang" 484 | 485 | "fmt" 486 | ) 487 | `, 488 | }, 489 | { 490 | "default-order", 491 | 492 | `sections: 493 | - Standard 494 | - Prefix(github.com/daixiang0) 495 | - Default 496 | `, 497 | `package main 498 | 499 | import ( 500 | "fmt" 501 | 502 | g "github.com/golang" 503 | 504 | "github.com/daixiang0/a" 505 | ) 506 | `, 507 | `package main 508 | 509 | import ( 510 | "fmt" 511 | 512 | g "github.com/golang" 513 | 514 | "github.com/daixiang0/a" 515 | ) 516 | `, 517 | }, 518 | { 519 | "dot-and-blank", 520 | 521 | `sections: 522 | - Standard 523 | - Default 524 | - Prefix(github.com/daixiang0) 525 | - Blank 526 | - Dot 527 | `, 528 | `package main 529 | 530 | import ( 531 | "fmt" 532 | 533 | g "github.com/golang" 534 | . "github.com/golang/dot" 535 | _ "github.com/golang/blank" 536 | 537 | "github.com/daixiang0/a" 538 | "github.com/daixiang0/gci" 539 | "github.com/daixiang0/gci/subtest" 540 | . "github.com/daixiang0/gci/dot" 541 | _ "github.com/daixiang0/gci/blank" 542 | ) 543 | `, 544 | `package main 545 | 546 | import ( 547 | "fmt" 548 | 549 | g "github.com/golang" 550 | 551 | "github.com/daixiang0/a" 552 | "github.com/daixiang0/gci" 553 | "github.com/daixiang0/gci/subtest" 554 | 555 | _ "github.com/daixiang0/gci/blank" 556 | _ "github.com/golang/blank" 557 | 558 | . "github.com/daixiang0/gci/dot" 559 | . "github.com/golang/dot" 560 | ) 561 | `, 562 | }, 563 | { 564 | "duplicate-imports", 565 | 566 | `sections: 567 | - Standard 568 | - Default 569 | - Prefix(github.com/daixiang0) 570 | `, 571 | `package main 572 | 573 | import ( 574 | "fmt" 575 | 576 | g "github.com/golang" 577 | 578 | a "github.com/daixiang0/gci" 579 | "github.com/daixiang0/gci" 580 | ) 581 | `, 582 | `package main 583 | 584 | import ( 585 | "fmt" 586 | 587 | g "github.com/golang" 588 | 589 | "github.com/daixiang0/gci" 590 | a "github.com/daixiang0/gci" 591 | ) 592 | `, 593 | }, 594 | { 595 | "grouped-multiple-custom", 596 | 597 | `sections: 598 | - Standard 599 | - Default 600 | - Prefix(github.com/daixiang0,gitlab.com/daixiang0,daixiang0) 601 | `, 602 | `package main 603 | 604 | import ( 605 | "daixiang0/lib1" 606 | "fmt" 607 | "github.com/daixiang0/gci" 608 | "gitlab.com/daixiang0/gci" 609 | g "github.com/golang" 610 | "github.com/daixiang0/gci/subtest" 611 | ) 612 | `, 613 | `package main 614 | 615 | import ( 616 | "fmt" 617 | 618 | g "github.com/golang" 619 | 620 | "daixiang0/lib1" 621 | "github.com/daixiang0/gci" 622 | "github.com/daixiang0/gci/subtest" 623 | "gitlab.com/daixiang0/gci" 624 | ) 625 | `, 626 | }, 627 | { 628 | "leading-comment", 629 | 630 | commonConfig, 631 | 632 | `package main 633 | 634 | import ( 635 | // foo 636 | "fmt" 637 | ) 638 | `, 639 | `package main 640 | 641 | import ( 642 | // foo 643 | "fmt" 644 | ) 645 | `, 646 | }, 647 | { 648 | "linebreak", 649 | 650 | `sections: 651 | - Standard 652 | - Default 653 | - Prefix(github.com/daixiang0) 654 | `, 655 | `package main 656 | 657 | import ( 658 | g "github.com/golang" 659 | 660 | "fmt" 661 | 662 | "github.com/daixiang0/gci" 663 | 664 | ) 665 | `, 666 | `package main 667 | 668 | import ( 669 | "fmt" 670 | 671 | g "github.com/golang" 672 | 673 | "github.com/daixiang0/gci" 674 | ) 675 | `, 676 | }, 677 | { 678 | "linebreak-no-custom", 679 | 680 | `sections: 681 | - Standard 682 | - Default 683 | - Prefix(github.com/daixiang0) 684 | `, 685 | `package main 686 | 687 | import ( 688 | g "github.com/golang" 689 | 690 | "fmt" 691 | 692 | ) 693 | `, 694 | `package main 695 | 696 | import ( 697 | "fmt" 698 | 699 | g "github.com/golang" 700 | ) 701 | `, 702 | }, 703 | { 704 | "mismatch-section", 705 | 706 | `sections: 707 | - Standard 708 | - Default 709 | - Prefix(github.com/daixiang0) 710 | - Prefix(github.com/daixiang0/gci) 711 | `, 712 | `package main 713 | 714 | import ( 715 | "fmt" 716 | 717 | g "github.com/golang" 718 | 719 | "github.com/daixiang0/gci" 720 | ) 721 | `, 722 | `package main 723 | 724 | import ( 725 | "fmt" 726 | 727 | g "github.com/golang" 728 | 729 | "github.com/daixiang0/gci" 730 | ) 731 | `, 732 | }, 733 | { 734 | "multiple-custom", 735 | 736 | `sections: 737 | - Standard 738 | - Default 739 | - Prefix(github.com/daixiang0) 740 | - Prefix(github.com/daixiang0/gci) 741 | - Prefix(github.com/daixiang0/gci/subtest) 742 | `, 743 | `package main 744 | 745 | import ( 746 | "fmt" 747 | 748 | g "github.com/golang" 749 | 750 | "github.com/daixiang0/a" 751 | "github.com/daixiang0/gci" 752 | "github.com/daixiang0/gci/subtest" 753 | ) 754 | `, 755 | `package main 756 | 757 | import ( 758 | "fmt" 759 | 760 | g "github.com/golang" 761 | 762 | "github.com/daixiang0/a" 763 | 764 | "github.com/daixiang0/gci" 765 | 766 | "github.com/daixiang0/gci/subtest" 767 | ) 768 | `, 769 | }, 770 | { 771 | "multiple-imports", 772 | 773 | commonConfig, 774 | 775 | `package main 776 | 777 | import "fmt" 778 | 779 | import "context" 780 | 781 | import ( 782 | "os" 783 | 784 | "github.com/daixiang0/test" 785 | ) 786 | 787 | import "math" 788 | 789 | 790 | // main 791 | func main() { 792 | } 793 | `, 794 | `package main 795 | 796 | import ( 797 | "context" 798 | "fmt" 799 | "math" 800 | "os" 801 | 802 | "github.com/daixiang0/test" 803 | ) 804 | 805 | // main 806 | func main() { 807 | } 808 | `, 809 | }, 810 | { 811 | "multiple-line-comment", 812 | 813 | commonConfig, 814 | 815 | `package proc 816 | 817 | import ( 818 | "context" // in-line comment 819 | "fmt" 820 | "os" 821 | 822 | //nolint:depguard // A multi-line comment explaining why in 823 | // this one case it's OK to use os/exec even though depguard 824 | // is configured to force us to use dlib/exec instead. 825 | "os/exec" 826 | 827 | "golang.org/x/sys/unix" 828 | "github.com/local/dlib/dexec" 829 | ) 830 | `, 831 | `package proc 832 | 833 | import ( 834 | "context" // in-line comment 835 | "fmt" 836 | "os" 837 | //nolint:depguard // A multi-line comment explaining why in 838 | // this one case it's OK to use os/exec even though depguard 839 | // is configured to force us to use dlib/exec instead. 840 | "os/exec" 841 | 842 | "github.com/local/dlib/dexec" 843 | "golang.org/x/sys/unix" 844 | ) 845 | `, 846 | }, 847 | { 848 | "nochar-after-import", 849 | 850 | commonConfig, 851 | 852 | `package main 853 | 854 | import ( 855 | "fmt" 856 | ) 857 | `, 858 | `package main 859 | 860 | import ( 861 | "fmt" 862 | ) 863 | `, 864 | }, 865 | { 866 | "no-format", 867 | 868 | commonConfig, 869 | 870 | `package main 871 | 872 | import( 873 | "fmt" 874 | 875 | g "github.com/golang" 876 | 877 | "github.com/daixiang0/gci" 878 | ) 879 | `, 880 | `package main 881 | 882 | import ( 883 | "fmt" 884 | 885 | g "github.com/golang" 886 | 887 | "github.com/daixiang0/gci" 888 | ) 889 | `, 890 | }, 891 | { 892 | "nolint", 893 | 894 | commonConfig, 895 | 896 | `package main 897 | 898 | import ( 899 | "fmt" 900 | 901 | "github.com/forbidden/pkg" //nolint:depguard 902 | 903 | _ "github.com/daixiang0/gci" //nolint:depguard 904 | ) 905 | `, 906 | `package main 907 | 908 | import ( 909 | "fmt" 910 | 911 | "github.com/forbidden/pkg" //nolint:depguard 912 | 913 | _ "github.com/daixiang0/gci" //nolint:depguard 914 | ) 915 | `, 916 | }, 917 | { 918 | "number-in-alias", 919 | 920 | commonConfig, 921 | 922 | `package main 923 | 924 | import ( 925 | "fmt" 926 | 927 | go_V1 "github.com/golang" 928 | 929 | "github.com/daixiang0/gci" 930 | ) 931 | `, 932 | `package main 933 | 934 | import ( 935 | "fmt" 936 | 937 | go_V1 "github.com/golang" 938 | 939 | "github.com/daixiang0/gci" 940 | ) 941 | `, 942 | }, 943 | { 944 | "one-import", 945 | 946 | commonConfig, 947 | 948 | `package main 949 | import ( 950 | "fmt" 951 | ) 952 | 953 | func main() { 954 | } 955 | `, 956 | `package main 957 | import ( 958 | "fmt" 959 | ) 960 | 961 | func main() { 962 | } 963 | `, 964 | }, 965 | { 966 | "one-import-one-line", 967 | 968 | commonConfig, 969 | 970 | `package main 971 | 972 | import "fmt" 973 | 974 | func main() { 975 | } 976 | `, 977 | `package main 978 | 979 | import "fmt" 980 | 981 | func main() { 982 | } 983 | `, 984 | }, 985 | { 986 | "one-line-import-after-import", 987 | 988 | `sections: 989 | - Standard 990 | - Default 991 | - Prefix(github.com/daixiang0) 992 | `, 993 | `package main 994 | 995 | import ( 996 | "fmt" 997 | "os" 998 | 999 | "github.com/daixiang0/test" 1000 | ) 1001 | 1002 | import "context" 1003 | `, 1004 | `package main 1005 | 1006 | import ( 1007 | "context" 1008 | "fmt" 1009 | "os" 1010 | 1011 | "github.com/daixiang0/test" 1012 | ) 1013 | `, 1014 | }, 1015 | { 1016 | "same-prefix-custom", 1017 | 1018 | `sections: 1019 | - Standard 1020 | - Default 1021 | - Prefix(github.com/daixiang0/gci) 1022 | - Prefix(github.com/daixiang0/gci/subtest) 1023 | `, 1024 | `package main 1025 | 1026 | import ( 1027 | "fmt" 1028 | 1029 | g "github.com/golang" 1030 | 1031 | "github.com/daixiang0/gci" 1032 | "github.com/daixiang0/gci/subtest" 1033 | ) 1034 | `, 1035 | `package main 1036 | 1037 | import ( 1038 | "fmt" 1039 | 1040 | g "github.com/golang" 1041 | 1042 | "github.com/daixiang0/gci" 1043 | 1044 | "github.com/daixiang0/gci/subtest" 1045 | ) 1046 | `, 1047 | }, 1048 | { 1049 | "simple-case", 1050 | 1051 | commonConfig, 1052 | 1053 | `package main 1054 | 1055 | import ( 1056 | "golang.org/x/tools" 1057 | 1058 | "fmt" 1059 | 1060 | "github.com/daixiang0/gci" 1061 | ) 1062 | `, 1063 | `package main 1064 | 1065 | import ( 1066 | "fmt" 1067 | 1068 | "golang.org/x/tools" 1069 | 1070 | "github.com/daixiang0/gci" 1071 | ) 1072 | `, 1073 | }, 1074 | { 1075 | "whitespace-test", 1076 | 1077 | commonConfig, 1078 | 1079 | `package main 1080 | 1081 | import ( 1082 | "fmt" 1083 | "github.com/golang" // golang 1084 | alias "github.com/daixiang0/gci" 1085 | ) 1086 | `, 1087 | `package main 1088 | 1089 | import ( 1090 | "fmt" 1091 | 1092 | "github.com/golang" // golang 1093 | 1094 | alias "github.com/daixiang0/gci" 1095 | ) 1096 | `, 1097 | }, 1098 | { 1099 | "with-above-comment-and-alias", 1100 | 1101 | commonConfig, 1102 | 1103 | `package main 1104 | 1105 | import ( 1106 | "fmt" 1107 | // golang 1108 | _ "github.com/golang" 1109 | "github.com/daixiang0/gci" 1110 | ) 1111 | `, 1112 | `package main 1113 | 1114 | import ( 1115 | "fmt" 1116 | 1117 | // golang 1118 | _ "github.com/golang" 1119 | 1120 | "github.com/daixiang0/gci" 1121 | ) 1122 | `, 1123 | }, 1124 | { 1125 | "with-comment-and-alias", 1126 | 1127 | commonConfig, 1128 | 1129 | `package main 1130 | 1131 | import ( 1132 | "fmt" 1133 | _ "github.com/golang" // golang 1134 | "github.com/daixiang0/gci" 1135 | ) 1136 | `, 1137 | `package main 1138 | 1139 | import ( 1140 | "fmt" 1141 | 1142 | _ "github.com/golang" // golang 1143 | 1144 | "github.com/daixiang0/gci" 1145 | ) 1146 | `, 1147 | }, 1148 | { 1149 | "same-prefix-custom", 1150 | 1151 | `sections: 1152 | - Standard 1153 | - Default 1154 | - Prefix(github.com/daixiang0/gci) 1155 | - Prefix(github.com/daixiang0/gci/subtest) 1156 | `, 1157 | `package main 1158 | 1159 | import ( 1160 | "fmt" 1161 | 1162 | g "github.com/golang" 1163 | 1164 | "github.com/daixiang0/gci" 1165 | "github.com/daixiang0/gci/subtest" 1166 | ) 1167 | `, 1168 | `package main 1169 | 1170 | import ( 1171 | "fmt" 1172 | 1173 | g "github.com/golang" 1174 | 1175 | "github.com/daixiang0/gci" 1176 | 1177 | "github.com/daixiang0/gci/subtest" 1178 | ) 1179 | `, 1180 | }, 1181 | { 1182 | "same-prefix-custom", 1183 | 1184 | `sections: 1185 | - Standard 1186 | - Default 1187 | - Prefix(github.com/daixiang0/gci) 1188 | - Prefix(github.com/daixiang0/gci/subtest) 1189 | `, 1190 | `package main 1191 | 1192 | import ( 1193 | "fmt" 1194 | 1195 | g "github.com/golang" 1196 | 1197 | "github.com/daixiang0/gci" 1198 | "github.com/daixiang0/gci/subtest" 1199 | ) 1200 | `, 1201 | `package main 1202 | 1203 | import ( 1204 | "fmt" 1205 | 1206 | g "github.com/golang" 1207 | 1208 | "github.com/daixiang0/gci" 1209 | 1210 | "github.com/daixiang0/gci/subtest" 1211 | ) 1212 | `, 1213 | }, 1214 | { 1215 | "blank-in-config", 1216 | 1217 | `sections: 1218 | - Standard 1219 | - Default 1220 | - Prefix( github.com/daixiang0/gci, github.com/daixiang0/gci/subtest ) 1221 | `, 1222 | `package main 1223 | 1224 | import ( 1225 | "fmt" 1226 | 1227 | g "github.com/golang" 1228 | 1229 | "github.com/daixiang0/gci" 1230 | "github.com/daixiang0/gci/subtest" 1231 | ) 1232 | `, 1233 | `package main 1234 | 1235 | import ( 1236 | "fmt" 1237 | 1238 | g "github.com/golang" 1239 | 1240 | "github.com/daixiang0/gci" 1241 | "github.com/daixiang0/gci/subtest" 1242 | ) 1243 | `, 1244 | }, 1245 | { 1246 | "alias", 1247 | 1248 | `sections: 1249 | - Standard 1250 | - Default 1251 | - Alias 1252 | `, 1253 | `package main 1254 | 1255 | import ( 1256 | testing "github.com/daixiang0/test" 1257 | "fmt" 1258 | 1259 | g "github.com/golang" 1260 | 1261 | "github.com/daixiang0/gci" 1262 | "github.com/daixiang0/gci/subtest" 1263 | ) 1264 | `, 1265 | `package main 1266 | 1267 | import ( 1268 | "fmt" 1269 | 1270 | "github.com/daixiang0/gci" 1271 | "github.com/daixiang0/gci/subtest" 1272 | 1273 | testing "github.com/daixiang0/test" 1274 | g "github.com/golang" 1275 | ) 1276 | `, 1277 | }, 1278 | { 1279 | "no-trailing-newline", 1280 | 1281 | `sections: 1282 | - Standard 1283 | `, 1284 | `package main 1285 | 1286 | import ( 1287 | "net" 1288 | "fmt" 1289 | )`, 1290 | `package main 1291 | 1292 | import ( 1293 | "fmt" 1294 | "net" 1295 | ) 1296 | `, 1297 | }, 1298 | } 1299 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/.gitattributes: -------------------------------------------------------------------------------- 1 | # try and stop git running on Windows from converting line endings from 2 | # in all Go files under this directory. This is to avoid inconsistent test 3 | # results when `gci` strips "\r" characters 4 | **/*.go eol=lf 5 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/config.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | - Standard 3 | - Default 4 | - LocalModule 5 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/simple 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/internal/bar/lib.go: -------------------------------------------------------------------------------- 1 | package bar 2 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/internal/foo/lib.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "example.com/simple/internal/bar" 5 | "log" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/internal/foo/lib.out.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "log" 5 | 6 | "example.com/simple/internal/bar" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/internal/lib.go: -------------------------------------------------------------------------------- 1 | package internal 2 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/main.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "golang.org/x/net" 5 | "example.com/simple/internal" 6 | "example.com/simple/internal/foo" 7 | "example.com/simple/internal/bar" 8 | "log" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module/main.out.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/net" 7 | 8 | "example.com/simple/internal" 9 | "example.com/simple/internal/bar" 10 | "example.com/simple/internal/foo" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/.gitattributes: -------------------------------------------------------------------------------- 1 | # try and stop git running on Windows from converting line endings from 2 | # in all Go files under this directory. This is to avoid inconsistent test 3 | # results when `gci` strips "\r" characters 4 | **/*.go eol=lf 5 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "example.com/service/internal" 5 | "example.com/service/internal/bar" 6 | "example.com/service/internal/foo" 7 | "golang.org/x/net" 8 | "log" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/cmd/client/main.out.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/net" 7 | 8 | "example.com/service/internal" 9 | "example.com/service/internal/bar" 10 | "example.com/service/internal/foo" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "example.com/service/internal" 5 | "example.com/service/internal/bar" 6 | "example.com/service/internal/foo" 7 | "golang.org/x/net" 8 | "log" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/cmd/server/main.out.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/net" 7 | 8 | "example.com/service/internal" 9 | "example.com/service/internal/bar" 10 | "example.com/service/internal/foo" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/config.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | - Standard 3 | - Default 4 | - LocalModule 5 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/service 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/internal/bar/lib.go: -------------------------------------------------------------------------------- 1 | package bar 2 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/internal/foo/lib.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "example.com/service/internal/bar" 5 | "log" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/internal/foo/lib.out.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "log" 5 | 6 | "example.com/service/internal/bar" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_canonical/internal/lib.go: -------------------------------------------------------------------------------- 1 | package internal 2 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/.gitattributes: -------------------------------------------------------------------------------- 1 | # try and stop git running on Windows from converting line endings from 2 | # in all Go files under this directory. This is to avoid inconsistent test 3 | # results when `gci` strips "\r" characters 4 | **/*.go eol=lf 5 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/net" 5 | "log" 6 | "service/internal" 7 | "service/internal/bar" 8 | "service/internal/foo" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/cmd/client/main.out.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/net" 7 | 8 | "service/internal" 9 | "service/internal/bar" 10 | "service/internal/foo" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/net" 5 | "log" 6 | "service/internal" 7 | "service/internal/bar" 8 | "service/internal/foo" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/cmd/server/main.out.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "golang.org/x/net" 7 | 8 | "service/internal" 9 | "service/internal/bar" 10 | "service/internal/foo" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/config.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | - Standard 3 | - Default 4 | - LocalModule 5 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/go.mod: -------------------------------------------------------------------------------- 1 | module service 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/internal/bar/lib.go: -------------------------------------------------------------------------------- 1 | package bar 2 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/internal/foo/lib.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "log" 5 | "service/internal/bar" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/internal/foo/lib.out.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "log" 5 | 6 | "service/internal/bar" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/gci/testdata/module_noncanonical/internal/lib.go: -------------------------------------------------------------------------------- 1 | package internal 2 | -------------------------------------------------------------------------------- /pkg/io/file.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import "io/ioutil" 4 | 5 | // FileObj allows mocking the access to files 6 | type FileObj interface { 7 | Load() ([]byte, error) 8 | Path() string 9 | } 10 | 11 | // File represents a file that can be loaded from the file system 12 | type File struct { 13 | FilePath string 14 | } 15 | 16 | func (f File) Path() string { 17 | return f.FilePath 18 | } 19 | 20 | func (f File) Load() ([]byte, error) { 21 | return ioutil.ReadFile(f.FilePath) 22 | } 23 | 24 | // FileGeneratorFunc returns a list of files that can be loaded and processed 25 | type FileGeneratorFunc func() ([]FileObj, error) 26 | 27 | func (a FileGeneratorFunc) Combine(b FileGeneratorFunc) FileGeneratorFunc { 28 | return func() ([]FileObj, error) { 29 | files, err := a() 30 | if err != nil { 31 | return nil, err 32 | } 33 | additionalFiles, err := b() 34 | if err != nil { 35 | return nil, err 36 | } 37 | files = append(files, additionalFiles...) 38 | return files, err 39 | } 40 | } 41 | 42 | func GoFilesInPathsGenerator(paths []string, skipVendor bool) FileGeneratorFunc { 43 | checkFunc := isGoFile 44 | if skipVendor { 45 | checkFunc = checkChains(isGoFile, isOutsideVendorDir) 46 | } 47 | 48 | return FilesInPathsGenerator(paths, checkFunc) 49 | } 50 | 51 | func FilesInPathsGenerator(paths []string, fileCheckFun fileCheckFunction) FileGeneratorFunc { 52 | return func() (foundFiles []FileObj, err error) { 53 | for _, path := range paths { 54 | files, err := FindFilesForPath(path, fileCheckFun) 55 | if err != nil { 56 | return nil, err 57 | } 58 | for _, filePath := range files { 59 | foundFiles = append(foundFiles, File{filePath}) 60 | } 61 | } 62 | return foundFiles, nil 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/io/search.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type fileCheckFunction func(path string, file os.FileInfo) bool 10 | 11 | func FindFilesForPath(path string, fileCheckFun fileCheckFunction) ([]string, error) { 12 | switch entry, err := os.Stat(path); { 13 | case err != nil: 14 | return nil, err 15 | case entry.IsDir(): 16 | return findFilesForDirectory(path, fileCheckFun) 17 | case fileCheckFun(path, entry): 18 | return []string{filepath.Clean(path)}, nil 19 | default: 20 | return []string{}, nil 21 | } 22 | } 23 | 24 | func findFilesForDirectory(dirPath string, fileCheckFun fileCheckFunction) ([]string, error) { 25 | var filePaths []string 26 | err := filepath.WalkDir(dirPath, func(path string, entry fs.DirEntry, err error) error { 27 | if err != nil { 28 | return err 29 | } 30 | file, err := entry.Info() 31 | if err != nil { 32 | return err 33 | } 34 | if !entry.IsDir() && fileCheckFun(path, file) { 35 | filePaths = append(filePaths, filepath.Clean(path)) 36 | } 37 | return nil 38 | }) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return filePaths, nil 43 | } 44 | 45 | func isGoFile(_ string, file os.FileInfo) bool { 46 | return !file.IsDir() && filepath.Ext(file.Name()) == ".go" 47 | } 48 | 49 | func isOutsideVendorDir(path string, _ os.FileInfo) bool { 50 | for { 51 | base := filepath.Base(path) 52 | if base == "vendor" { 53 | return false 54 | } 55 | 56 | prevPath := path 57 | path = filepath.Dir(path) 58 | 59 | if prevPath == path { 60 | break 61 | } 62 | } 63 | 64 | return true 65 | } 66 | 67 | func checkChains(funcs ...fileCheckFunction) fileCheckFunction { 68 | return func(path string, file os.FileInfo) bool { 69 | for _, checkFunc := range funcs { 70 | if !checkFunc(path, file) { 71 | return false 72 | } 73 | } 74 | 75 | return true 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/io/stdin.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | type stdInFile struct{} 9 | 10 | func (s stdInFile) Load() ([]byte, error) { 11 | return ioutil.ReadAll(os.Stdin) 12 | } 13 | 14 | func (s stdInFile) Path() string { 15 | return "StdIn" 16 | } 17 | 18 | var StdInGenerator FileGeneratorFunc = func() ([]FileObj, error) { 19 | stat, err := os.Stdin.Stat() 20 | if err != nil { 21 | return nil, err 22 | } 23 | if (stat.Mode() & os.ModeCharDevice) == 0 { 24 | return []FileObj{stdInFile{}}, nil 25 | } 26 | return []FileObj{}, nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "sync" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // Use L to log with Zap 11 | var logger *zap.Logger 12 | 13 | // Keep the config to reference the atomicLevel for changing levels 14 | var logConfig zap.Config 15 | 16 | var doOnce sync.Once 17 | 18 | // InitLogger sets up the logger 19 | func InitLogger() { 20 | doOnce.Do(func() { 21 | logConfig = zap.NewDevelopmentConfig() 22 | 23 | logConfig.EncoderConfig.TimeKey = "timestamp" 24 | logConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 25 | logConfig.Level.SetLevel(zapcore.InfoLevel) 26 | logConfig.OutputPaths = []string{"stderr"} 27 | 28 | var err error 29 | logger, err = logConfig.Build() 30 | if err != nil { 31 | panic(err) 32 | } 33 | }) 34 | } 35 | 36 | // SetLevel allows you to set the level of the default gci logger. 37 | // This will not work if you replace the logger 38 | func SetLevel(level zapcore.Level) { 39 | logConfig.Level.SetLevel(level) 40 | } 41 | 42 | // L returns the logger 43 | func L() *zap.Logger { 44 | return logger 45 | } 46 | 47 | // SetLogger allows you to set the logger to whatever you want 48 | func SetLogger(l *zap.Logger) { 49 | logger = l 50 | } 51 | -------------------------------------------------------------------------------- /pkg/parse/parse.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | const C = "\"C\"" 12 | 13 | type GciImports struct { 14 | // original index of import group, include doc, name, path and comment 15 | Start, End int 16 | Name, Path string 17 | } 18 | type ImportList []*GciImports 19 | 20 | func (l ImportList) Len() int { 21 | return len(l) 22 | } 23 | 24 | func (l ImportList) Less(i, j int) bool { 25 | if strings.Compare(l[i].Path, l[j].Path) == 0 { 26 | return strings.Compare(l[i].Name, l[j].Name) < 0 27 | } 28 | 29 | return strings.Compare(l[i].Path, l[j].Path) < 0 30 | } 31 | 32 | func (l ImportList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 33 | 34 | /* 35 | * AST considers a import block as below: 36 | * ``` 37 | * Doc 38 | * Name Path Comment 39 | * ``` 40 | * An example is like below: 41 | * ``` 42 | * // test 43 | * test "fmt" // test 44 | * ``` 45 | * getImports return a import block with name, start and end index 46 | */ 47 | func getImports(imp *ast.ImportSpec) (start, end int, name string) { 48 | if imp.Doc != nil { 49 | // doc poc need minus one to get the first index of comment 50 | start = int(imp.Doc.Pos()) - 1 51 | } else { 52 | if imp.Name != nil { 53 | // name pos need minus one too 54 | start = int(imp.Name.Pos()) - 1 55 | } else { 56 | // path pos start without quote, need minus one for it 57 | start = int(imp.Path.Pos()) - 1 58 | } 59 | } 60 | 61 | if imp.Name != nil { 62 | name = imp.Name.Name 63 | } 64 | 65 | if imp.Comment != nil { 66 | end = int(imp.Comment.End()) 67 | } else { 68 | end = int(imp.Path.End()) 69 | } 70 | return 71 | } 72 | 73 | func ParseFile(src []byte, filename string) (ImportList, int, int, int, int, error) { 74 | fileSet := token.NewFileSet() 75 | f, err := parser.ParseFile(fileSet, filename, src, parser.ParseComments) 76 | if err != nil { 77 | return nil, 0, 0, 0, 0, err 78 | } 79 | 80 | if len(f.Imports) == 0 { 81 | return nil, 0, 0, 0, 0, NoImportError{} 82 | } 83 | 84 | var ( 85 | // headEnd means the start of import block 86 | headEnd int 87 | // tailStart means the end + 1 of import block 88 | tailStart int 89 | // cStart means the start of C import block 90 | cStart int 91 | // cEnd means the end of C import block 92 | cEnd int 93 | data ImportList 94 | ) 95 | 96 | for index, decl := range f.Decls { 97 | switch decl.(type) { 98 | // skip BadDecl and FuncDecl 99 | case *ast.GenDecl: 100 | genDecl := decl.(*ast.GenDecl) 101 | 102 | if genDecl.Tok == token.IMPORT { 103 | // there are two cases, both end with linebreak: 104 | // 1. 105 | // import ( 106 | // "xxxx" 107 | // ) 108 | // 2. 109 | // import "xxx" 110 | if headEnd == 0 { 111 | headEnd = int(decl.Pos()) - 1 112 | } 113 | tailStart = int(decl.End()) 114 | if tailStart > len(src) { 115 | tailStart = len(src) 116 | } 117 | 118 | for _, spec := range genDecl.Specs { 119 | imp := spec.(*ast.ImportSpec) 120 | // there are only one C import block 121 | // ensure C import block is the first import block 122 | if imp.Path.Value == C { 123 | /* 124 | common case: 125 | 126 | // #include 127 | import "C" 128 | 129 | notice that decl.Pos() == genDecl.Pos() > genDecl.Doc.Pos() 130 | */ 131 | if genDecl.Doc != nil { 132 | cStart = int(genDecl.Doc.Pos()) - 1 133 | // if C import block is the first, update headEnd 134 | if index == 0 { 135 | headEnd = cStart 136 | } 137 | } else { 138 | /* 139 | special case: 140 | 141 | import "C" 142 | */ 143 | cStart = int(decl.Pos()) - 1 144 | } 145 | 146 | cEnd = int(decl.End()) 147 | 148 | continue 149 | } 150 | 151 | start, end, name := getImports(imp) 152 | 153 | data = append(data, &GciImports{ 154 | Start: start, 155 | End: end, 156 | Name: name, 157 | Path: strings.Trim(imp.Path.Value, `"`), 158 | }) 159 | } 160 | } 161 | } 162 | } 163 | 164 | sort.Sort(data) 165 | return data, headEnd, tailStart, cStart, cEnd, nil 166 | } 167 | 168 | // IsGeneratedFileByComment reports whether the source file is generated code. 169 | // Using a bit laxer rules than https://golang.org/s/generatedcode to 170 | // match more generated code. 171 | // Taken from https://github.com/golangci/golangci-lint. 172 | func IsGeneratedFileByComment(in string) bool { 173 | const ( 174 | genCodeGenerated = "code generated" 175 | genDoNotEdit = "do not edit" 176 | genAutoFile = "autogenerated file" // easyjson 177 | genAutoGenerated = "automatically generated" // genny 178 | ) 179 | 180 | markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile, genAutoGenerated} 181 | in = strings.ToLower(in) 182 | for _, marker := range markers { 183 | if strings.Contains(in, marker) { 184 | return true 185 | } 186 | } 187 | 188 | return false 189 | } 190 | 191 | type NoImportError struct{} 192 | 193 | func (n NoImportError) Error() string { 194 | return "No imports" 195 | } 196 | 197 | func (i NoImportError) Is(err error) bool { 198 | _, ok := err.(NoImportError) 199 | return ok 200 | } 201 | -------------------------------------------------------------------------------- /pkg/section/alias.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | type Alias struct{} 9 | 10 | const AliasType = "alias" 11 | 12 | func (b Alias) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 13 | if spec.Name != "." && spec.Name != "_" && spec.Name != "" { 14 | return specificity.NameMatch{} 15 | } 16 | return specificity.MisMatch{} 17 | } 18 | 19 | func (b Alias) String() string { 20 | return AliasType 21 | } 22 | 23 | func (b Alias) Type() string { 24 | return AliasType 25 | } 26 | -------------------------------------------------------------------------------- /pkg/section/blank.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | type Blank struct{} 9 | 10 | const BlankType = "blank" 11 | 12 | func (b Blank) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 13 | if spec.Name == "_" { 14 | return specificity.NameMatch{} 15 | } 16 | return specificity.MisMatch{} 17 | } 18 | 19 | func (b Blank) String() string { 20 | return BlankType 21 | } 22 | 23 | func (b Blank) Type() string { 24 | return BlankType 25 | } 26 | -------------------------------------------------------------------------------- /pkg/section/commentline.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/daixiang0/gci/pkg/parse" 7 | "github.com/daixiang0/gci/pkg/specificity" 8 | ) 9 | 10 | type CommentLine struct { 11 | Comment string 12 | } 13 | 14 | func (c CommentLine) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 15 | return specificity.MisMatch{} 16 | } 17 | 18 | func (c CommentLine) String() string { 19 | return fmt.Sprintf("commentline(%s)", c.Comment) 20 | } 21 | 22 | func (c CommentLine) Type() string { 23 | return "commentline" 24 | } 25 | -------------------------------------------------------------------------------- /pkg/section/commentline_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/daixiang0/gci/pkg/specificity" 7 | ) 8 | 9 | func TestCommentLineSpecificity(t *testing.T) { 10 | testCases := []specificityTestData{ 11 | {`""`, CommentLine{""}, specificity.MisMatch{}}, 12 | {`"x"`, CommentLine{""}, specificity.MisMatch{}}, 13 | {`"//"`, CommentLine{""}, specificity.MisMatch{}}, 14 | {`"/"`, CommentLine{""}, specificity.MisMatch{}}, 15 | } 16 | testSpecificity(t, testCases) 17 | } 18 | 19 | // func TestCommentLineToString(t *testing.T) { 20 | // testSectionToString(t, CommentLine{""}) 21 | // testSectionToString(t, CommentLine{"abc"}) 22 | // } 23 | -------------------------------------------------------------------------------- /pkg/section/default.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | const DefaultType = "default" 9 | 10 | type Default struct{} 11 | 12 | func (d Default) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 13 | return specificity.Default{} 14 | } 15 | 16 | func (d Default) String() string { 17 | return DefaultType 18 | } 19 | 20 | func (d Default) Type() string { 21 | return DefaultType 22 | } 23 | -------------------------------------------------------------------------------- /pkg/section/default_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/daixiang0/gci/pkg/specificity" 7 | ) 8 | 9 | func TestDefaultSpecificity(t *testing.T) { 10 | testCases := []specificityTestData{ 11 | {`""`, Default{}, specificity.Default{}}, 12 | {`"x"`, Default{}, specificity.Default{}}, 13 | } 14 | testSpecificity(t, testCases) 15 | } 16 | 17 | // func TestDefaultSectionParsing(t *testing.T) { 18 | // testCases := []sectionTestData{ 19 | // {"def", Default{}, nil}, 20 | // {"defAult(invalid)", nil, SectionTypeDoesNotAcceptParametersError}, 21 | // } 22 | // testSectionParser(t, testCases) 23 | // } 24 | 25 | // func TestDefaultSectionToString(t *testing.T) { 26 | // testSectionToString(t, Default{}) 27 | // } 28 | -------------------------------------------------------------------------------- /pkg/section/dot.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | type Dot struct{} 9 | 10 | const DotType = "dot" 11 | 12 | func (d Dot) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 13 | if spec.Name == "." { 14 | return specificity.NameMatch{} 15 | } 16 | return specificity.MisMatch{} 17 | } 18 | 19 | func (d Dot) String() string { 20 | return DotType 21 | } 22 | 23 | func (d Dot) Type() string { 24 | return DotType 25 | } 26 | -------------------------------------------------------------------------------- /pkg/section/errors.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/daixiang0/gci/pkg/parse" 8 | "github.com/daixiang0/gci/pkg/utils" 9 | ) 10 | 11 | type SectionParsingError struct { 12 | error 13 | } 14 | 15 | func (s SectionParsingError) Unwrap() error { 16 | return s.error 17 | } 18 | 19 | func (s SectionParsingError) Wrap(sectionStr string) error { 20 | return fmt.Errorf("failed to parse section %q: %w", sectionStr, s) 21 | } 22 | 23 | func (s SectionParsingError) Is(err error) bool { 24 | _, ok := err.(SectionParsingError) 25 | return ok 26 | } 27 | 28 | var MissingParameterClosingBracketsError = fmt.Errorf("section parameter is missing closing %q", utils.RightParenthesis) 29 | 30 | var MoreThanOneOpeningQuotesError = fmt.Errorf("found more than one %q parameter start sequences", utils.RightParenthesis) 31 | 32 | var SectionTypeDoesNotAcceptParametersError = errors.New("section type does not accept a parameter") 33 | 34 | var SectionTypeDoesNotAcceptPrefixError = errors.New("section may not contain a Prefix") 35 | 36 | var SectionTypeDoesNotAcceptSuffixError = errors.New("section may not contain a Suffix") 37 | 38 | type EqualSpecificityMatchError struct { 39 | Imports *parse.GciImports 40 | SectionA, SectionB Section 41 | } 42 | 43 | func (e EqualSpecificityMatchError) Error() string { 44 | return fmt.Sprintf("Import %v matched section %s and %s equally", e.Imports, e.SectionA, e.SectionB) 45 | } 46 | 47 | func (e EqualSpecificityMatchError) Is(err error) bool { 48 | _, ok := err.(EqualSpecificityMatchError) 49 | return ok 50 | } 51 | 52 | type NoMatchingSectionForImportError struct { 53 | Imports *parse.GciImports 54 | } 55 | 56 | func (n NoMatchingSectionForImportError) Error() string { 57 | return fmt.Sprintf("No section found for Import: %v", n.Imports) 58 | } 59 | 60 | func (n NoMatchingSectionForImportError) Is(err error) bool { 61 | _, ok := err.(NoMatchingSectionForImportError) 62 | return ok 63 | } 64 | 65 | type InvalidImportSplitError struct { 66 | segments []string 67 | } 68 | 69 | func (i InvalidImportSplitError) Error() string { 70 | return fmt.Sprintf("separating the inline comment from the import yielded an invalid number of segments: %v", i.segments) 71 | } 72 | 73 | func (i InvalidImportSplitError) Is(err error) bool { 74 | _, ok := err.(InvalidImportSplitError) 75 | return ok 76 | } 77 | 78 | type InvalidAliasSplitError struct { 79 | segments []string 80 | } 81 | 82 | func (i InvalidAliasSplitError) Error() string { 83 | return fmt.Sprintf("separating the alias from the path yielded an invalid number of segments: %v", i.segments) 84 | } 85 | 86 | func (i InvalidAliasSplitError) Is(err error) bool { 87 | _, ok := err.(InvalidAliasSplitError) 88 | return ok 89 | } 90 | 91 | var ( 92 | MissingImportStatementError = FileParsingError{errors.New("no import statement present in File")} 93 | ImportStatementNotClosedError = FileParsingError{errors.New("import statement not closed")} 94 | ) 95 | 96 | type FileParsingError struct { 97 | error 98 | } 99 | 100 | func (f FileParsingError) Unwrap() error { 101 | return f.error 102 | } 103 | 104 | func (f FileParsingError) Is(err error) bool { 105 | _, ok := err.(FileParsingError) 106 | return ok 107 | } 108 | -------------------------------------------------------------------------------- /pkg/section/errors_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestErrorMatching(t *testing.T) { 11 | assert.True(t, errors.Is(MissingParameterClosingBracketsError, MissingParameterClosingBracketsError)) 12 | assert.True(t, errors.Is(MoreThanOneOpeningQuotesError, MoreThanOneOpeningQuotesError)) 13 | assert.True(t, errors.Is(SectionTypeDoesNotAcceptParametersError, SectionTypeDoesNotAcceptParametersError)) 14 | assert.True(t, errors.Is(SectionTypeDoesNotAcceptPrefixError, SectionTypeDoesNotAcceptPrefixError)) 15 | assert.True(t, errors.Is(SectionTypeDoesNotAcceptSuffixError, SectionTypeDoesNotAcceptSuffixError)) 16 | } 17 | 18 | func TestErrorClass(t *testing.T) { 19 | subError := MissingParameterClosingBracketsError 20 | errorGroup := SectionParsingError{subError} 21 | assert.True(t, errors.Is(errorGroup, SectionParsingError{})) 22 | assert.True(t, errors.Is(errorGroup, subError)) 23 | assert.True(t, errors.Is(errorGroup.Wrap("x"), SectionParsingError{})) 24 | assert.True(t, errors.Is(errorGroup.Wrap("x"), subError)) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/section/local_module.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "golang.org/x/mod/modfile" 9 | 10 | "github.com/daixiang0/gci/pkg/parse" 11 | "github.com/daixiang0/gci/pkg/specificity" 12 | ) 13 | 14 | const LocalModuleType = "localmodule" 15 | 16 | type LocalModule struct { 17 | Path string 18 | } 19 | 20 | func (m *LocalModule) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 21 | if spec.Path == m.Path || strings.HasPrefix(spec.Path, m.Path+"/") { 22 | return specificity.LocalModule{} 23 | } 24 | 25 | return specificity.MisMatch{} 26 | } 27 | 28 | func (m *LocalModule) String() string { 29 | return LocalModuleType 30 | } 31 | 32 | func (m *LocalModule) Type() string { 33 | return LocalModuleType 34 | } 35 | 36 | // Configure configures the module section by finding the module 37 | // for the current path 38 | func (m *LocalModule) Configure(path string) error { 39 | if path != "" { 40 | m.Path = path 41 | } else { 42 | path, err := findLocalModule() 43 | if err != nil { 44 | return fmt.Errorf("finding local modules for `localModule` configuration: %w", err) 45 | } 46 | m.Path = path 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func findLocalModule() (string, error) { 53 | b, err := os.ReadFile("go.mod") 54 | if err != nil { 55 | return "", fmt.Errorf("reading go.mod: %w", err) 56 | } 57 | 58 | return modfile.ModulePath(b), nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/section/local_module_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/daixiang0/gci/pkg/specificity" 7 | ) 8 | 9 | func TestLocalModule_specificity(t *testing.T) { 10 | testCases := []specificityTestData{ 11 | {"example.com/hello", &LocalModule{Path: "example.com/hello"}, specificity.LocalModule{}}, 12 | {"example.com/hello/world", &LocalModule{Path: "example.com/hello"}, specificity.LocalModule{}}, 13 | {"example.com/hello-world", &LocalModule{Path: "example.com/hello"}, specificity.MisMatch{}}, 14 | {"example.com/helloworld", &LocalModule{Path: "example.com/hello"}, specificity.MisMatch{}}, 15 | {"example.com", &LocalModule{Path: "example.com/hello"}, specificity.MisMatch{}}, 16 | } 17 | testSpecificity(t, testCases) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/section/newline.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | const newLineName = "newline" 9 | 10 | type NewLine struct{} 11 | 12 | func (n NewLine) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 13 | return specificity.MisMatch{} 14 | } 15 | 16 | func (n NewLine) String() string { 17 | return newLineName 18 | } 19 | 20 | func (n NewLine) Type() string { 21 | return newLineName 22 | } 23 | -------------------------------------------------------------------------------- /pkg/section/newline_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/daixiang0/gci/pkg/specificity" 7 | ) 8 | 9 | func TestNewLineSpecificity(t *testing.T) { 10 | testCases := []specificityTestData{ 11 | {`""`, NewLine{}, specificity.MisMatch{}}, 12 | {`"x"`, NewLine{}, specificity.MisMatch{}}, 13 | {`"\n"`, NewLine{}, specificity.MisMatch{}}, 14 | } 15 | testSpecificity(t, testCases) 16 | } 17 | 18 | // func TestNewLineToString(t *testing.T) { 19 | // testSectionToString(t, NewLine{}) 20 | // } 21 | -------------------------------------------------------------------------------- /pkg/section/parser.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func Parse(data []string) (SectionList, error) { 10 | if len(data) == 0 { 11 | return nil, nil 12 | } 13 | 14 | var list SectionList 15 | var errString string 16 | for _, d := range data { 17 | s := strings.ToLower(d) 18 | if len(s) == 0 { 19 | return nil, nil 20 | } 21 | 22 | if s == "default" { 23 | list = append(list, Default{}) 24 | } else if s == "standard" { 25 | list = append(list, Standard{}) 26 | } else if s == "newline" { 27 | list = append(list, NewLine{}) 28 | } else if strings.HasPrefix(s, "prefix(") && len(d) > 8 { 29 | list = append(list, Custom{d[7 : len(d)-1]}) 30 | } else if strings.HasPrefix(s, "commentline(") && len(d) > 13 { 31 | list = append(list, Custom{d[12 : len(d)-1]}) 32 | } else if s == "dot" { 33 | list = append(list, Dot{}) 34 | } else if s == "blank" { 35 | list = append(list, Blank{}) 36 | } else if s == "alias" { 37 | list = append(list, Alias{}) 38 | } else if s == "localmodule" { 39 | // pointer because we need to mutate the section at configuration time 40 | list = append(list, &LocalModule{}) 41 | } else { 42 | errString += fmt.Sprintf(" %s", s) 43 | } 44 | } 45 | if errString != "" { 46 | return nil, errors.New(fmt.Sprintf("invalid params:%s", errString)) 47 | } 48 | return list, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/section/parser_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type sectionTestData struct { 11 | input []string 12 | expectedSection SectionList 13 | expectedError error 14 | } 15 | 16 | func TestParse(t *testing.T) { 17 | testCases := []sectionTestData{ 18 | { 19 | input: []string{""}, 20 | expectedSection: nil, 21 | expectedError: nil, 22 | }, 23 | { 24 | input: []string{"prefix(go)"}, 25 | expectedSection: SectionList{Custom{"go"}}, 26 | expectedError: nil, 27 | }, 28 | { 29 | input: []string{"prefix(go-UPPER-case)"}, 30 | expectedSection: SectionList{Custom{"go-UPPER-case"}}, 31 | expectedError: nil, 32 | }, 33 | { 34 | input: []string{"PREFIX(go-UPPER-case)"}, 35 | expectedSection: SectionList{Custom{"go-UPPER-case"}}, 36 | expectedError: nil, 37 | }, 38 | { 39 | input: []string{"prefix("}, 40 | expectedSection: nil, 41 | expectedError: errors.New("invalid params: prefix("), 42 | }, 43 | { 44 | input: []string{"prefix(domainA,domainB)"}, 45 | expectedSection: SectionList{Custom{"domainA,domainB"}}, 46 | expectedError: nil, 47 | }, 48 | } 49 | for _, test := range testCases { 50 | parsedSection, err := Parse(test.input) 51 | assert.Equal(t, test.expectedSection, parsedSection) 52 | assert.Equal(t, test.expectedError, err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/section/prefix.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/daixiang0/gci/pkg/parse" 8 | "github.com/daixiang0/gci/pkg/specificity" 9 | ) 10 | 11 | type Custom struct { 12 | Prefix string 13 | } 14 | 15 | // CustomSeparator allows you to group multiple custom prefix together in the same section 16 | // gci diff -s standard -s default -s prefix(github.com/company,gitlab.com/company,companysuffix) 17 | const CustomSeparator = "," 18 | 19 | const CustomType = "custom" 20 | 21 | func (c Custom) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 22 | for _, prefix := range strings.Split(c.Prefix, CustomSeparator) { 23 | prefix = strings.TrimSpace(prefix) 24 | if strings.HasPrefix(spec.Path, prefix) { 25 | return specificity.Match{Length: len(prefix)} 26 | } 27 | } 28 | 29 | return specificity.MisMatch{} 30 | } 31 | 32 | func (c Custom) String() string { 33 | return fmt.Sprintf("prefix(%s)", c.Prefix) 34 | } 35 | 36 | func (c Custom) Type() string { 37 | return CustomType 38 | } 39 | -------------------------------------------------------------------------------- /pkg/section/prefix_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | // func TestPrefixSpecificity(t *testing.T) { 4 | // testCases := []specificityTestData{ 5 | // {`"foo/pkg/bar"`, Prefix{"", nil, nil}, specificity.MisMatch{}}, 6 | // {`"foo/pkg/bar"`, Prefix{"foo", nil, nil}, specificity.Match{3}}, 7 | // {`"foo/pkg/bar"`, Prefix{"bar", nil, nil}, specificity.MisMatch{}}, 8 | // {`"foo/pkg/bar"`, Prefix{"github.com/foo/bar", nil, nil}, specificity.MisMatch{}}, 9 | // {`"foo/pkg/bar"`, Prefix{"github.com/foo", nil, nil}, specificity.MisMatch{}}, 10 | // {`"foo/pkg/bar"`, Prefix{"github.com/bar", nil, nil}, specificity.MisMatch{}}, 11 | // } 12 | // testSpecificity(t, testCases) 13 | // } 14 | 15 | // func TestPrefixParsing(t *testing.T) { 16 | // testCases := []sectionTestData{ 17 | // {"pkgPREFIX", Custom{"", nil, nil}, nil}, 18 | // {"prefix(test.com)", Custom{"test.com", nil, nil}, nil}, 19 | // } 20 | // testSectionParser(t, testCases) 21 | // } 22 | 23 | // func TestPrefixToString(t *testing.T) { 24 | // testSectionToString(t, Custom{}) 25 | // testSectionToString(t, Custom{"", nil, nil}) 26 | // testSectionToString(t, Custom{"abc.org", nil, nil}) 27 | // testSectionToString(t, Custom{"abc.org", nil, CommentLine{"a"}}) 28 | // testSectionToString(t, Custom{"abc.org", CommentLine{"a"}, NewLine{}}) 29 | // } 30 | -------------------------------------------------------------------------------- /pkg/section/section.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | // Section defines a part of the formatted output. 9 | type Section interface { 10 | // MatchSpecificity returns how well an Import matches to this Section 11 | MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity 12 | 13 | // String Implements the stringer interface 14 | String() string 15 | 16 | // return section type 17 | Type() string 18 | } 19 | 20 | type SectionList []Section 21 | 22 | func (list SectionList) String() []string { 23 | var output []string 24 | for _, section := range list { 25 | output = append(output, section.String()) 26 | } 27 | return output 28 | } 29 | 30 | func DefaultSections() SectionList { 31 | return SectionList{Standard{}, Default{}} 32 | } 33 | 34 | func DefaultSectionSeparators() SectionList { 35 | return SectionList{NewLine{}} 36 | } 37 | -------------------------------------------------------------------------------- /pkg/section/section_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/daixiang0/gci/pkg/parse" 8 | "github.com/daixiang0/gci/pkg/specificity" 9 | ) 10 | 11 | type specificityTestData struct { 12 | path string 13 | section Section 14 | expectedSpecificity specificity.MatchSpecificity 15 | } 16 | 17 | func testSpecificity(t *testing.T, testCases []specificityTestData) { 18 | for _, test := range testCases { 19 | testName := fmt.Sprintf("%s:%v", test.path, test.section) 20 | t.Run(testName, testSpecificityCase(test)) 21 | } 22 | } 23 | 24 | func testSpecificityCase(testData specificityTestData) func(t *testing.T) { 25 | return func(t *testing.T) { 26 | t.Parallel() 27 | detectedSpecificity := testData.section.MatchSpecificity(&parse.GciImports{Path: testData.path}) 28 | if detectedSpecificity != testData.expectedSpecificity { 29 | t.Errorf("Specificity is %v and not %v", detectedSpecificity, testData.expectedSpecificity) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/section/standard.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/daixiang0/gci/pkg/parse" 5 | "github.com/daixiang0/gci/pkg/specificity" 6 | ) 7 | 8 | const StandardType = "standard" 9 | 10 | type Standard struct{} 11 | 12 | func (s Standard) MatchSpecificity(spec *parse.GciImports) specificity.MatchSpecificity { 13 | if isStandard(spec.Path) { 14 | return specificity.StandardMatch{} 15 | } 16 | return specificity.MisMatch{} 17 | } 18 | 19 | func (s Standard) String() string { 20 | return StandardType 21 | } 22 | 23 | func (s Standard) Type() string { 24 | return StandardType 25 | } 26 | 27 | func isStandard(pkg string) bool { 28 | _, ok := standardPackages[pkg] 29 | return ok 30 | } 31 | -------------------------------------------------------------------------------- /pkg/section/standard_list.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | // Code generated based on go1.24.1 X:boringcrypto,arenas,synctest. DO NOT EDIT. 4 | 5 | var standardPackages = map[string]struct{}{ 6 | "archive/tar": {}, 7 | "archive/zip": {}, 8 | "arena": {}, 9 | "bufio": {}, 10 | "bytes": {}, 11 | "cmp": {}, 12 | "compress/bzip2": {}, 13 | "compress/flate": {}, 14 | "compress/gzip": {}, 15 | "compress/lzw": {}, 16 | "compress/zlib": {}, 17 | "container/heap": {}, 18 | "container/list": {}, 19 | "container/ring": {}, 20 | "context": {}, 21 | "crypto": {}, 22 | "crypto/aes": {}, 23 | "crypto/boring": {}, 24 | "crypto/cipher": {}, 25 | "crypto/des": {}, 26 | "crypto/dsa": {}, 27 | "crypto/ecdh": {}, 28 | "crypto/ecdsa": {}, 29 | "crypto/ed25519": {}, 30 | "crypto/elliptic": {}, 31 | "crypto/fips140": {}, 32 | "crypto/hkdf": {}, 33 | "crypto/hmac": {}, 34 | "crypto/md5": {}, 35 | "crypto/mlkem": {}, 36 | "crypto/pbkdf2": {}, 37 | "crypto/rand": {}, 38 | "crypto/rc4": {}, 39 | "crypto/rsa": {}, 40 | "crypto/sha1": {}, 41 | "crypto/sha256": {}, 42 | "crypto/sha3": {}, 43 | "crypto/sha512": {}, 44 | "crypto/subtle": {}, 45 | "crypto/tls": {}, 46 | "crypto/tls/fipsonly": {}, 47 | "crypto/x509": {}, 48 | "crypto/x509/pkix": {}, 49 | "database/sql": {}, 50 | "database/sql/driver": {}, 51 | "debug/buildinfo": {}, 52 | "debug/dwarf": {}, 53 | "debug/elf": {}, 54 | "debug/gosym": {}, 55 | "debug/macho": {}, 56 | "debug/pe": {}, 57 | "debug/plan9obj": {}, 58 | "embed": {}, 59 | "encoding": {}, 60 | "encoding/ascii85": {}, 61 | "encoding/asn1": {}, 62 | "encoding/base32": {}, 63 | "encoding/base64": {}, 64 | "encoding/binary": {}, 65 | "encoding/csv": {}, 66 | "encoding/gob": {}, 67 | "encoding/hex": {}, 68 | "encoding/json": {}, 69 | "encoding/pem": {}, 70 | "encoding/xml": {}, 71 | "errors": {}, 72 | "expvar": {}, 73 | "flag": {}, 74 | "fmt": {}, 75 | "go/ast": {}, 76 | "go/build": {}, 77 | "go/build/constraint": {}, 78 | "go/constant": {}, 79 | "go/doc": {}, 80 | "go/doc/comment": {}, 81 | "go/format": {}, 82 | "go/importer": {}, 83 | "go/parser": {}, 84 | "go/printer": {}, 85 | "go/scanner": {}, 86 | "go/token": {}, 87 | "go/types": {}, 88 | "go/version": {}, 89 | "hash": {}, 90 | "hash/adler32": {}, 91 | "hash/crc32": {}, 92 | "hash/crc64": {}, 93 | "hash/fnv": {}, 94 | "hash/maphash": {}, 95 | "html": {}, 96 | "html/template": {}, 97 | "image": {}, 98 | "image/color": {}, 99 | "image/color/palette": {}, 100 | "image/draw": {}, 101 | "image/gif": {}, 102 | "image/jpeg": {}, 103 | "image/png": {}, 104 | "index/suffixarray": {}, 105 | "io": {}, 106 | "io/fs": {}, 107 | "io/ioutil": {}, 108 | "iter": {}, 109 | "log": {}, 110 | "log/slog": {}, 111 | "log/syslog": {}, 112 | "maps": {}, 113 | "math": {}, 114 | "math/big": {}, 115 | "math/bits": {}, 116 | "math/cmplx": {}, 117 | "math/rand": {}, 118 | "math/rand/v2": {}, 119 | "mime": {}, 120 | "mime/multipart": {}, 121 | "mime/quotedprintable": {}, 122 | "net": {}, 123 | "net/http": {}, 124 | "net/http/cgi": {}, 125 | "net/http/cookiejar": {}, 126 | "net/http/fcgi": {}, 127 | "net/http/httptest": {}, 128 | "net/http/httptrace": {}, 129 | "net/http/httputil": {}, 130 | "net/http/pprof": {}, 131 | "net/mail": {}, 132 | "net/netip": {}, 133 | "net/rpc": {}, 134 | "net/rpc/jsonrpc": {}, 135 | "net/smtp": {}, 136 | "net/textproto": {}, 137 | "net/url": {}, 138 | "os": {}, 139 | "os/exec": {}, 140 | "os/signal": {}, 141 | "os/user": {}, 142 | "path": {}, 143 | "path/filepath": {}, 144 | "plugin": {}, 145 | "reflect": {}, 146 | "regexp": {}, 147 | "regexp/syntax": {}, 148 | "runtime": {}, 149 | "runtime/cgo": {}, 150 | "runtime/coverage": {}, 151 | "runtime/debug": {}, 152 | "runtime/metrics": {}, 153 | "runtime/pprof": {}, 154 | "runtime/race": {}, 155 | "runtime/trace": {}, 156 | "slices": {}, 157 | "sort": {}, 158 | "strconv": {}, 159 | "strings": {}, 160 | "structs": {}, 161 | "sync": {}, 162 | "sync/atomic": {}, 163 | "syscall": {}, 164 | "syscall/js": {}, 165 | "testing": {}, 166 | "testing/fstest": {}, 167 | "testing/iotest": {}, 168 | "testing/quick": {}, 169 | "testing/slogtest": {}, 170 | "text/scanner": {}, 171 | "text/tabwriter": {}, 172 | "text/template": {}, 173 | "text/template/parse": {}, 174 | "time": {}, 175 | "time/tzdata": {}, 176 | "unicode": {}, 177 | "unicode/utf16": {}, 178 | "unicode/utf8": {}, 179 | "unique": {}, 180 | "unsafe": {}, 181 | "weak": {}, 182 | } 183 | -------------------------------------------------------------------------------- /pkg/section/standard_test.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/daixiang0/gci/pkg/specificity" 7 | ) 8 | 9 | func TestStandardPackageSpecificity(t *testing.T) { 10 | testCases := []specificityTestData{ 11 | {"context", Standard{}, specificity.StandardMatch{}}, 12 | {"contexts", Standard{}, specificity.MisMatch{}}, 13 | {"crypto", Standard{}, specificity.StandardMatch{}}, 14 | {"crypto1", Standard{}, specificity.MisMatch{}}, 15 | {"crypto/ae", Standard{}, specificity.MisMatch{}}, 16 | {"crypto/aes", Standard{}, specificity.StandardMatch{}}, 17 | {"crypto/aes2", Standard{}, specificity.MisMatch{}}, 18 | {"syscall/js", Standard{}, specificity.StandardMatch{}}, 19 | } 20 | testSpecificity(t, testCases) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/specificity/default.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | type Default struct{} 4 | 5 | func (d Default) IsMoreSpecific(than MatchSpecificity) bool { 6 | return isMoreSpecific(d, than) 7 | } 8 | 9 | func (d Default) Equal(to MatchSpecificity) bool { 10 | return equalSpecificity(d, to) 11 | } 12 | 13 | func (d Default) class() specificityClass { 14 | return DefaultClass 15 | } 16 | 17 | func (d Default) String() string { 18 | return "Default" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/specificity/local_module.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | type LocalModule struct{} 4 | 5 | func (m LocalModule) IsMoreSpecific(than MatchSpecificity) bool { 6 | return isMoreSpecific(m, than) 7 | } 8 | 9 | func (m LocalModule) Equal(to MatchSpecificity) bool { 10 | return equalSpecificity(m, to) 11 | } 12 | 13 | func (LocalModule) class() specificityClass { 14 | return LocalModuleClass 15 | } 16 | -------------------------------------------------------------------------------- /pkg/specificity/match.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | import "fmt" 4 | 5 | type Match struct { 6 | Length int 7 | } 8 | 9 | func (m Match) IsMoreSpecific(than MatchSpecificity) bool { 10 | otherMatch, isMatch := than.(Match) 11 | return isMoreSpecific(m, than) || (isMatch && m.Length > otherMatch.Length) 12 | } 13 | 14 | func (m Match) Equal(to MatchSpecificity) bool { 15 | return equalSpecificity(m, to) 16 | } 17 | 18 | func (m Match) class() specificityClass { 19 | return MatchClass 20 | } 21 | 22 | func (m Match) String() string { 23 | return fmt.Sprintf("Match(length: %d)", m.Length) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/specificity/mismatch.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | type MisMatch struct{} 4 | 5 | func (m MisMatch) IsMoreSpecific(than MatchSpecificity) bool { 6 | return isMoreSpecific(m, than) 7 | } 8 | 9 | func (m MisMatch) Equal(to MatchSpecificity) bool { 10 | return equalSpecificity(m, to) 11 | } 12 | 13 | func (m MisMatch) class() specificityClass { 14 | return MisMatchClass 15 | } 16 | 17 | func (m MisMatch) String() string { 18 | return "Mismatch" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/specificity/name.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | type NameMatch struct{} 4 | 5 | func (n NameMatch) IsMoreSpecific(than MatchSpecificity) bool { 6 | return isMoreSpecific(n, than) 7 | } 8 | 9 | func (n NameMatch) Equal(to MatchSpecificity) bool { 10 | return equalSpecificity(n, to) 11 | } 12 | 13 | func (n NameMatch) class() specificityClass { 14 | return NameClass 15 | } 16 | 17 | func (n NameMatch) String() string { 18 | return "Name" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/specificity/specificity.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | type specificityClass int 4 | 5 | const ( 6 | MisMatchClass = 0 7 | DefaultClass = 10 8 | StandardClass = 20 9 | MatchClass = 30 10 | NameClass = 40 11 | LocalModuleClass = 50 12 | ) 13 | 14 | // MatchSpecificity is used to determine which section matches an import best 15 | type MatchSpecificity interface { 16 | IsMoreSpecific(than MatchSpecificity) bool 17 | Equal(to MatchSpecificity) bool 18 | class() specificityClass 19 | } 20 | 21 | func isMoreSpecific(this, than MatchSpecificity) bool { 22 | return this.class() > than.class() 23 | } 24 | 25 | func equalSpecificity(base, to MatchSpecificity) bool { 26 | // m.class() == to.class() would not work for Match 27 | return !base.IsMoreSpecific(to) && !to.IsMoreSpecific(base) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/specificity/specificity_test.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSpecificityOrder(t *testing.T) { 11 | testCases := testCasesInSpecificityOrder() 12 | for i := 1; i < len(testCases); i++ { 13 | t.Run(fmt.Sprintf("Specificity(%v)>Specificity(%v)", testCases[i], testCases[i-1]), func(t *testing.T) { 14 | assert.True(t, testCases[i].IsMoreSpecific(testCases[i-1])) 15 | }) 16 | } 17 | } 18 | 19 | func TestSpecificityEquality(t *testing.T) { 20 | for _, testCase := range testCasesInSpecificityOrder() { 21 | t.Run(fmt.Sprintf("Specificity(%v)==Specificity(%v)", testCase, testCase), func(t *testing.T) { 22 | assert.True(t, testCase.Equal(testCase)) 23 | }) 24 | } 25 | } 26 | 27 | func testCasesInSpecificityOrder() []MatchSpecificity { 28 | return []MatchSpecificity{MisMatch{}, Default{}, StandardMatch{}, Match{0}, Match{1}} 29 | } 30 | -------------------------------------------------------------------------------- /pkg/specificity/standard.go: -------------------------------------------------------------------------------- 1 | package specificity 2 | 3 | type StandardMatch struct{} 4 | 5 | func (s StandardMatch) IsMoreSpecific(than MatchSpecificity) bool { 6 | return isMoreSpecific(s, than) 7 | } 8 | 9 | func (s StandardMatch) Equal(to MatchSpecificity) bool { 10 | return equalSpecificity(s, to) 11 | } 12 | 13 | func (s StandardMatch) class() specificityClass { 14 | return StandardClass 15 | } 16 | 17 | func (s StandardMatch) String() string { 18 | return "Standard" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/utils/constants.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | Indent = '\t' 5 | Linebreak = '\n' 6 | WinLinebreak = '\r' 7 | 8 | Colon = ":" 9 | 10 | LeftParenthesis = '(' 11 | RightParenthesis = ')' 12 | ) 13 | --------------------------------------------------------------------------------