Skip to content

Commit 5f8864e

Browse files
authored
Prepare linter for golangci-lint (#5)
# golangci/golangci-lint#4196 1. TestdataDir() -> analysistest.TestData() 2. Described False-Negative cases as testcases and in README.md 3. Fixed link for type_nested.go.skip in README.md 4. Added testcase with generic 5. Added testcase with alias 6. Added testcase with map, chan, array, func # golangci/golangci-lint#4196 7. Added glob syntax for pkgs 8. Renamed options 9. Extended unexpected code message 10. Added unimplemented testcases
1 parent ce8cbf4 commit 5f8864e

File tree

36 files changed

+665
-150
lines changed

36 files changed

+665
-150
lines changed

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ Installation
1818

1919
### Options
2020

21-
- `-b`, `--blockedPkgs` - list of packages, where the structures should be created by factories. By default, all structures in all packages should be created by factories, [tests](testdata/src/factory/blockedPkgs).
22-
- `-ob`, `onlyBlockedPkgs` - only blocked packages should use factory to initiate struct, [tests](testdata/src/factory/onlyBlockedPkgs).
21+
- `--packageGlobs` - list of glob packages, which can create structures without factories inside the glob package. By default, all structures from another package should be created by factories, [tests](testdata/src/factory/packageGlobs).
22+
- `onlyPackageGlobs` - use a factory to initiate a structure for glob packages only, [tests](testdata/src/factory/onlyPackageGlobs).
2323

2424
## Example
2525

@@ -120,8 +120,28 @@ func nextID() int64 {
120120
</td></tr>
121121
</tbody></table>
122122

123+
## False Negative
124+
125+
Linter doesn't catch some cases.
126+
127+
1. Buffered channel. You can initialize struct in line `v, ok := <-bufCh` [example](testdata/src/factory/unimplemented/chan.go).
128+
2. Local initialization, [example](testdata/src/factory/unimplemented/local/).
129+
3. Named return. If you want to block that case, you can use [nonamedreturns](https://github.com/firefart/nonamedreturns) linter, [example](testdata/src/factory/unimplemented/named_return.go).
130+
4. var declaration, `var initilized nested.Struct` gives structure without factory, [example](testdata/src/factory/unimplemented/var.go).
131+
5. Casting to nested struct, [example](testdata/src/factory/unimplemented/casting/).
132+
123133
## TODO
124134

135+
### Possible Features
136+
137+
1. Catch nested struct in the same package, [example](testdata/src/factory/unimplemented/local/nested_struct.go).
138+
```go
139+
return Struct{
140+
Other: OtherStruct{}, // want `Use factory for nested.Struct`
141+
}
142+
```
143+
2. Resolve false negative issue with `var declaration`.
144+
125145
### Features that are difficult to implement and unplanned
126146

127-
1. Type assertion, type declaration and type underlying, [tests](testdata/src/factory/default/type_nested.go.skip).
147+
1. Type assertion, type declaration and type underlying, [tests](testdata/src/factory/simple/type_nested.go.skip).

factory.go

Lines changed: 113 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import (
44
"fmt"
55
"go/ast"
66
"go/types"
7+
"log/slog"
78
"strings"
89

10+
"github.com/gobwas/glob"
911
"golang.org/x/tools/go/analysis"
1012
"golang.org/x/tools/go/analysis/passes/inspect"
1113
)
1214

1315
type config struct {
14-
blockedPkgs stringsFlag
15-
onlyBlockedPkgs bool
16+
pkgGlobs stringsFlag
17+
onlyPkgGlobs bool
1618
}
1719

1820
type stringsFlag []string
@@ -28,21 +30,18 @@ func (s *stringsFlag) Set(value string) error {
2830
}
2931

3032
func (s stringsFlag) Value() []string {
31-
blockedPkgs := make([]string, 0, len(s))
33+
res := make([]string, 0, len(s))
3234

33-
for _, pgk := range s {
34-
pgk = strings.TrimSpace(pgk)
35-
pgk = strings.TrimSuffix(pgk, "/") + "/"
36-
37-
blockedPkgs = append(blockedPkgs, pgk)
35+
for _, str := range s {
36+
res = append(res, strings.TrimSpace(str))
3837
}
3938

40-
return blockedPkgs
39+
return res
4140
}
4241

4342
const (
44-
blockedPkgsDesc = "List of packages, which should use factory to initiate struct."
45-
onlyBlockedPkgsDesc = "Only blocked packages should use factory to initiate struct."
43+
packageGlobsDesc = "List of glob packages, which can create structures without factories inside the glob package"
44+
onlyPkgGlobsDesc = "Use a factory to initiate a structure for glob packages only."
4645
)
4746

4847
func NewAnalyzer() *analysis.Analyzer {
@@ -54,11 +53,9 @@ func NewAnalyzer() *analysis.Analyzer {
5453

5554
cfg := config{}
5655

57-
analyzer.Flags.Var(&cfg.blockedPkgs, "b", blockedPkgsDesc)
58-
analyzer.Flags.Var(&cfg.blockedPkgs, "blockedPkgs", blockedPkgsDesc)
56+
analyzer.Flags.Var(&cfg.pkgGlobs, "packageGlobs", packageGlobsDesc)
5957

60-
analyzer.Flags.BoolVar(&cfg.onlyBlockedPkgs, "ob", false, onlyBlockedPkgsDesc)
61-
analyzer.Flags.BoolVar(&cfg.onlyBlockedPkgs, "onlyBlockedPkgs", false, onlyBlockedPkgsDesc)
58+
analyzer.Flags.BoolVar(&cfg.onlyPkgGlobs, "onlyPackageGlobs", false, onlyPkgGlobsDesc)
6259

6360
analyzer.Run = run(&cfg)
6461

@@ -68,14 +65,19 @@ func NewAnalyzer() *analysis.Analyzer {
6865
func run(cfg *config) func(pass *analysis.Pass) (interface{}, error) {
6966
return func(pass *analysis.Pass) (interface{}, error) {
7067
var blockedStrategy blockedStrategy = newAnotherPkg()
71-
if len(cfg.blockedPkgs) > 0 {
68+
if len(cfg.pkgGlobs) > 0 {
7269
defaultStrategy := blockedStrategy
73-
if cfg.onlyBlockedPkgs {
70+
if cfg.onlyPkgGlobs {
7471
defaultStrategy = newNilPkg()
7572
}
7673

74+
pkgGlobs, err := compileGlobs(cfg.pkgGlobs.Value())
75+
if err != nil {
76+
return nil, err
77+
}
78+
7779
blockedStrategy = newBlockedPkgs(
78-
cfg.blockedPkgs.Value(),
80+
pkgGlobs,
7981
defaultStrategy,
8082
)
8183
}
@@ -109,134 +111,146 @@ func (v *visitor) Visit(node ast.Node) ast.Visitor {
109111
return v
110112
}
111113

112-
var selExpr *ast.SelectorExpr
114+
compLitType := compLit.Type
113115

114116
// check []*Struct{{},&Struct}
115-
arr, isArr := compLit.Type.(*ast.ArrayType)
116-
if isArr && len(compLit.Elts) > 0 {
117-
v.checkSlice(arr, compLit)
117+
slice, isMap := compLitType.(*ast.ArrayType)
118+
if isMap && len(compLit.Elts) > 0 {
119+
v.checkSlice(slice, compLit)
118120

119121
return v
120122
}
121123

122-
ident, ok := compLit.Type.(*ast.Ident)
123-
if !ok {
124-
selExpr, ok = compLit.Type.(*ast.SelectorExpr)
125-
if !ok {
126-
return v
127-
}
124+
// check map[Struct]Struct{{}:{}}
125+
mp, isMap := compLitType.(*ast.MapType)
126+
if isMap {
127+
v.checkMap(mp, compLit)
128128

129-
ident = selExpr.Sel
129+
return v
130130
}
131131

132+
// check Struct{}
133+
ident := v.getIdent(compLitType)
132134
identObj := v.pass.TypesInfo.ObjectOf(ident)
135+
133136
if identObj == nil {
134137
return v
135138
}
136139

137140
if v.blockedStrategy.IsBlocked(v.pass.Pkg, identObj) {
138-
v.report(node, identObj)
141+
v.report(ident, identObj)
139142
}
140143

141144
return v
142145
}
143146

144-
func (v *visitor) checkSlice(arr *ast.ArrayType, compLit *ast.CompositeLit) {
145-
arrElt := arr.Elt
146-
if starExpr, ok := arr.Elt.(*ast.StarExpr); ok {
147-
arrElt = starExpr.X
147+
func (v *visitor) getIdent(expr ast.Expr) *ast.Ident {
148+
// pointer *Struct{}
149+
if starExpr, ok := expr.(*ast.StarExpr); ok {
150+
expr = starExpr.X
148151
}
149152

150-
selExpr, ok := arrElt.(*ast.SelectorExpr)
151-
if !ok {
152-
return
153+
// generic Struct[any]{}
154+
indexExpr, ok := expr.(*ast.IndexExpr)
155+
if ok {
156+
expr = indexExpr.X
153157
}
154158

155-
identObj := v.pass.TypesInfo.ObjectOf(selExpr.Sel)
156-
if identObj != nil {
157-
for _, elt := range compLit.Elts {
158-
eltCompLit, ok := elt.(*ast.CompositeLit)
159-
if ok && eltCompLit.Type == nil {
160-
if v.blockedStrategy.IsBlocked(v.pass.Pkg, identObj) {
161-
v.report(elt, identObj)
162-
}
163-
}
164-
}
159+
selExpr, ok := expr.(*ast.SelectorExpr)
160+
if !ok {
161+
return nil
165162
}
166-
}
167163

168-
func (v *visitor) report(node ast.Node, obj types.Object) {
169-
v.pass.Reportf(
170-
node.Pos(),
171-
fmt.Sprintf(`Use factory for %s.%s`, obj.Pkg().Name(), obj.Name()),
172-
)
164+
return selExpr.Sel
173165
}
174166

175-
type blockedStrategy interface {
176-
IsBlocked(currentPkg *types.Package, identObj types.Object) bool
177-
}
167+
func (v *visitor) checkSlice(arr *ast.ArrayType, compLit *ast.CompositeLit) {
168+
ident := v.getIdent(arr.Elt)
169+
identObj := v.pass.TypesInfo.ObjectOf(ident)
178170

179-
type nilPkg struct{}
171+
if identObj == nil {
172+
return
173+
}
180174

181-
func newNilPkg() nilPkg {
182-
return nilPkg{}
175+
for _, elt := range compLit.Elts {
176+
v.checkBrackets(elt, identObj)
177+
}
183178
}
184179

185-
func (nilPkg) IsBlocked(_ *types.Package, _ types.Object) bool {
186-
return false
187-
}
180+
func (v *visitor) checkMap(mp *ast.MapType, compLit *ast.CompositeLit) {
181+
keyIdent := v.getIdent(mp.Key)
182+
keyIdentObj := v.pass.TypesInfo.ObjectOf(keyIdent)
188183

189-
type anotherPkg struct{}
184+
valueIdent := v.getIdent(mp.Value)
185+
valueIdentObj := v.pass.TypesInfo.ObjectOf(valueIdent)
190186

191-
func newAnotherPkg() anotherPkg {
192-
return anotherPkg{}
193-
}
187+
if keyIdentObj == nil && valueIdentObj == nil {
188+
return
189+
}
194190

195-
func (anotherPkg) IsBlocked(
196-
currentPkg *types.Package,
197-
identObj types.Object,
198-
) bool {
199-
return currentPkg.Path() != identObj.Pkg().Path()
200-
}
191+
for _, elt := range compLit.Elts {
192+
keyValueExpr, ok := elt.(*ast.KeyValueExpr)
193+
if !ok {
194+
v.unexpectedCode(elt)
201195

202-
type blockedPkgs struct {
203-
blockedPkgs []string
204-
defaultStrategy blockedStrategy
205-
}
196+
continue
197+
}
206198

207-
func newBlockedPkgs(
208-
pkgs []string,
209-
defaultStrategy blockedStrategy,
210-
) blockedPkgs {
211-
return blockedPkgs{
212-
blockedPkgs: pkgs,
213-
defaultStrategy: defaultStrategy,
199+
v.checkBrackets(keyValueExpr.Key, keyIdentObj)
200+
v.checkBrackets(keyValueExpr.Value, valueIdentObj)
214201
}
215202
}
216203

217-
func (b blockedPkgs) IsBlocked(
218-
currentPkg *types.Package,
219-
identObj types.Object,
220-
) bool {
221-
identPkgPath := identObj.Pkg().Path() + "/"
222-
currentPkgPath := currentPkg.Path() + "/"
204+
// checkBrackets check {} in array, slice, map.
205+
func (v *visitor) checkBrackets(expr ast.Expr, identObj types.Object) {
206+
compLit, ok := expr.(*ast.CompositeLit)
207+
if ok && compLit.Type == nil && identObj != nil {
208+
if v.blockedStrategy.IsBlocked(v.pass.Pkg, identObj) {
209+
v.report(compLit, identObj)
210+
}
211+
}
212+
}
223213

224-
for _, blockedPkg := range b.blockedPkgs {
225-
isBlocked := strings.HasPrefix(identPkgPath, blockedPkg)
226-
isIncludedInBlocked := strings.HasPrefix(currentPkgPath, blockedPkg)
214+
func (v *visitor) report(node ast.Node, obj types.Object) {
215+
v.pass.Reportf(
216+
node.Pos(),
217+
fmt.Sprintf(`Use factory for %s.%s`, obj.Pkg().Name(), obj.Name()),
218+
)
219+
}
227220

228-
if isIncludedInBlocked {
229-
continue
230-
}
221+
func (v *visitor) unexpectedCode(node ast.Node) {
222+
fset := v.pass.Fset
223+
pos := fset.Position(node.Pos())
224+
slog.Error(
225+
fmt.Sprintf("Unexpected code in %s:%d:%d, please report to the developer with example.",
226+
fset.File(node.Pos()).Name(),
227+
pos.Line,
228+
pos.Column,
229+
),
230+
)
231+
}
231232

232-
if isBlocked {
233+
func containsMatchGlob(globs []glob.Glob, el string) bool {
234+
for _, g := range globs {
235+
if g.Match(el) {
233236
return true
234237
}
238+
}
235239

236-
if b.defaultStrategy.IsBlocked(currentPkg, identObj) {
237-
return true
240+
return false
241+
}
242+
243+
func compileGlobs(globs []string) ([]glob.Glob, error) {
244+
compiledGlobs := make([]glob.Glob, len(globs))
245+
246+
for idx, globString := range globs {
247+
glob, err := glob.Compile(globString)
248+
if err != nil {
249+
return nil, fmt.Errorf("unable to compile globs %s: %w", glob, err)
238250
}
251+
252+
compiledGlobs[idx] = glob
239253
}
240254

241-
return false
255+
return compiledGlobs, nil
242256
}

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ module github.com/maranqz/go-factory-lint
22

33
go 1.20
44

5-
require golang.org/x/tools v0.15.0
5+
require (
6+
github.com/gobwas/glob v0.2.3
7+
golang.org/x/tools v0.15.0
8+
)
69

710
require (
811
golang.org/x/mod v0.14.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
2+
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
13
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
24
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
35
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=

0 commit comments

Comments
 (0)