Skip to content

Commit 97eda8f

Browse files
committed
Refactor struct field selection to Inspector helper
1 parent 9fe9ade commit 97eda8f

File tree

5 files changed

+283
-0
lines changed

5 files changed

+283
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package inspector
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
7+
"golang.org/x/tools/go/analysis"
8+
"golang.org/x/tools/go/analysis/passes/inspect"
9+
astinspector "golang.org/x/tools/go/ast/inspector"
10+
11+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
12+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
13+
)
14+
15+
const name = "inspector"
16+
17+
var (
18+
errCouldNotGetInspector = errors.New("could not get inspector")
19+
errCouldNotGetJSONTags = errors.New("could not get json tags")
20+
errCouldNotGetMarkers = errors.New("could not get markers")
21+
)
22+
23+
// Analyzer is the analyzer for the inspector package.
24+
// It provides common functionality for analyzers that need to inspect fields and struct.
25+
// Abstracting away filtering of fields that the analyzers should and shouldn't be worrying about.
26+
var Analyzer = &analysis.Analyzer{
27+
Name: name,
28+
Doc: "Provides common functionality for analyzers that need to inspect fields and struct",
29+
Run: run,
30+
Requires: []*analysis.Analyzer{inspect.Analyzer, extractjsontags.Analyzer, markers.Analyzer},
31+
ResultType: reflect.TypeOf(newInspector(nil, nil, nil)),
32+
}
33+
34+
func run(pass *analysis.Pass) (interface{}, error) {
35+
astinspector, ok := pass.ResultOf[inspect.Analyzer].(*astinspector.Inspector)
36+
if !ok {
37+
return nil, errCouldNotGetInspector
38+
}
39+
40+
jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags)
41+
if !ok {
42+
return nil, errCouldNotGetJSONTags
43+
}
44+
45+
markersAccess, ok := pass.ResultOf[markers.Analyzer].(markers.Markers)
46+
if !ok {
47+
return nil, errCouldNotGetMarkers
48+
}
49+
50+
return newInspector(astinspector, jsonTags, markersAccess), nil
51+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package inspector_test
2+
3+
import (
4+
"errors"
5+
"go/ast"
6+
"testing"
7+
8+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
9+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/inspector"
10+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
11+
"golang.org/x/tools/go/analysis"
12+
"golang.org/x/tools/go/analysis/analysistest"
13+
)
14+
15+
func TestInspector(t *testing.T) {
16+
testdata := analysistest.TestData()
17+
18+
analysistest.Run(t, testdata, testAnalyzer, "a")
19+
}
20+
21+
var errCouldNotGetInspector = errors.New("could not get inspector")
22+
23+
var testAnalyzer = &analysis.Analyzer{
24+
Name: "test",
25+
Doc: "tests the inspector analyzer",
26+
Run: run,
27+
Requires: []*analysis.Analyzer{inspector.Analyzer},
28+
}
29+
30+
func run(pass *analysis.Pass) (interface{}, error) {
31+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
32+
if !ok {
33+
return nil, errCouldNotGetInspector
34+
}
35+
36+
inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
37+
var fieldName string
38+
if len(field.Names) > 0 {
39+
fieldName = field.Names[0].Name
40+
} else if ident, ok := field.Type.(*ast.Ident); ok {
41+
fieldName = ident.Name
42+
}
43+
44+
pass.Reportf(field.Pos(), "field: %v", fieldName)
45+
46+
if jsonTagInfo.Name != "" {
47+
pass.Reportf(field.Pos(), "json tag: %v", jsonTagInfo.Name)
48+
}
49+
})
50+
51+
return nil, nil //nolint:nilnil
52+
}

pkg/analysis/helpers/inspector/doc.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
inspector is a helper package that iterates over fields in structs, calling an inspection function on fields
3+
that should be considered for analysis.
4+
5+
The inspector extracts common logic of iterating and filtering through struct fields, so that analyzers
6+
need not re-implement the same filtering over and over.
7+
8+
For example, the inspector filters out struct definitions that are not type declarations, and fields that are ignored.
9+
10+
Example:
11+
12+
type A struct {
13+
// This field is included in the analysis.
14+
Field string `json:"field"`
15+
16+
// This field, and the fields within are ignored due to the json tag.
17+
F struct {
18+
Field string `json:"field"`
19+
} `json:"-"`
20+
}
21+
22+
// Any struct defined within a function is ignored.
23+
func Foo() {
24+
type Bar struct {
25+
Field string
26+
}
27+
}
28+
29+
// All fields within interface declarations are ignored.
30+
type Bar interface {
31+
Name() string
32+
}
33+
*/
34+
package inspector
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package inspector
2+
3+
import (
4+
"go/ast"
5+
"go/token"
6+
7+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
8+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
9+
astinspector "golang.org/x/tools/go/ast/inspector"
10+
)
11+
12+
// Inspector is an interface that allows for the inspection of fields in structs.
13+
type Inspector interface {
14+
// InspectFields is a function that iterates over fields in structs.
15+
InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers))
16+
}
17+
18+
// inspector implements the Inspector interface.
19+
type inspector struct {
20+
inspector *astinspector.Inspector
21+
jsonTags extractjsontags.StructFieldTags
22+
markers markers.Markers
23+
}
24+
25+
// newInspector creates a new inspector.
26+
func newInspector(astinspector *astinspector.Inspector, jsonTags extractjsontags.StructFieldTags, markers markers.Markers) Inspector {
27+
return &inspector{
28+
inspector: astinspector,
29+
jsonTags: jsonTags,
30+
markers: markers,
31+
}
32+
}
33+
34+
// InspectFields iterates over fields in structs, ignoring any struct that is not a type declaration, and any field that is ignored and
35+
// therefore would not be included in the CRD spec.
36+
// For the remaining fields, it calls the provided inspectField function to apply analysis logic.
37+
func (i *inspector) InspectFields(inspectField func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers)) {
38+
// Filter to fields so that we can iterate over fields in a struct.
39+
nodeFilter := []ast.Node{
40+
(*ast.Field)(nil),
41+
}
42+
43+
i.inspector.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
44+
if !push {
45+
return false
46+
}
47+
48+
if len(stack) < 3 {
49+
return true
50+
}
51+
52+
// The 0th node in the stack is the *ast.File.
53+
// The 1st node in the stack is the *ast.GenDecl.
54+
decl, ok := stack[1].(*ast.GenDecl)
55+
if !ok {
56+
// Make sure that we don't inspect structs within a function.
57+
return false
58+
}
59+
60+
if decl.Tok != token.TYPE {
61+
// Returning false here means we won't inspect non-type declarations (e.g. var, const, import).
62+
return false
63+
}
64+
65+
_, ok = stack[len(stack)-3].(*ast.StructType)
66+
if !ok {
67+
// A field within a struct has a FieldList parent and then a StructType parent.
68+
// If we don't have a StructType parent, then we're not in a struct.
69+
return false
70+
}
71+
72+
field, ok := n.(*ast.Field)
73+
if !ok {
74+
return true
75+
}
76+
77+
tagInfo := i.jsonTags.FieldTags(field)
78+
if tagInfo.Ignored {
79+
// Returning false here means we won't inspect the children of an ignored field.
80+
return false
81+
}
82+
83+
inspectField(field, stack, tagInfo, i.markers)
84+
85+
return true
86+
})
87+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package A
2+
3+
var (
4+
String string
5+
)
6+
7+
const (
8+
Int int = 0
9+
)
10+
11+
type A struct {
12+
Field string `json:"field"` // want "field: Field" "json tag: field"
13+
14+
B `json:"b"` // want "field: B" "json tag: b"
15+
16+
C `json:",inline"` // want "field: C"
17+
18+
D `json:"-"`
19+
20+
E struct { // want "field: E" "json tag: e"
21+
Field string `json:"field"` // want "field: Field" "json tag: field"
22+
} `json:"e"`
23+
24+
F struct {
25+
Field string `json:"field"`
26+
} `json:"-"`
27+
}
28+
29+
func (A) DoNothing() {}
30+
31+
type B struct {
32+
Field string `json:"field"` // want "field: Field" "json tag: field"
33+
}
34+
35+
type (
36+
C struct {
37+
Field string `json:"field"` // want "field: Field" "json tag: field"
38+
}
39+
40+
D struct {
41+
Field string `json:"field"` // want "field: Field" "json tag: field"
42+
}
43+
)
44+
45+
func Foo() {
46+
type Bar struct {
47+
Field string
48+
}
49+
}
50+
51+
type Bar interface {
52+
Name() string
53+
}
54+
55+
var Var = struct {
56+
Field string
57+
}{
58+
Field: "field",
59+
}

0 commit comments

Comments
 (0)