├── .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 |
--------------------------------------------------------------------------------