Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 15 additions & 60 deletions pkg/analysis/commentstart/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ import (
"strings"

"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
"github.com/JoelSpeed/kal/pkg/analysis/helpers/inspector"
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)

const name = "commentstart"

var (
errCouldNotGetInspector = errors.New("could not get inspector")
errCouldNotGetJSONTags = errors.New("could not get json tags")
)

// Analyzer is the analyzer for the commentstart package.
Expand All @@ -26,79 +25,37 @@ var Analyzer = &analysis.Analyzer{
Name: name,
Doc: "Check that all struct fields in an API have a godoc, and that the godoc starts with the serialised field name",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, extractjsontags.Analyzer},
Requires: []*analysis.Analyzer{inspector.Analyzer},
}

func run(pass *analysis.Pass) (interface{}, error) {
inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
if !ok {
return nil, errCouldNotGetInspector
}

jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags)
if !ok {
return nil, errCouldNotGetJSONTags
}

// Filter to structs so that we can iterate over fields in a struct.
nodeFilter := []ast.Node{
(*ast.Field)(nil),
}

inspect.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
if !push {
return false
}

if len(stack) < 2 {
return true
}

// The 0th node in the stack is the *ast.File.
// The 1st node in the stack is the *ast.GenDecl.
decl, ok := stack[1].(*ast.GenDecl)
if !ok {
return false
}

if decl.Tok != token.TYPE {
// Returning false here means we won't inspect non-type declarations (e.g. var, const, import).
return false
}

field, ok := n.(*ast.Field)
if !ok {
return true
}

return checkField(pass, field, jsonTags)
inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
checkField(pass, field, jsonTagInfo)
})

return nil, nil //nolint:nilnil
}

func checkField(pass *analysis.Pass, field *ast.Field, jsonTags extractjsontags.StructFieldTags) (proceed bool) {
if field == nil || len(field.Names) == 0 {
// Returning false here means we don't inspect inline fields.
// Types of inline fields will be inspected on its declaration.
return false
}

tagInfo := jsonTags.FieldTags(field)
if tagInfo.Ignored {
// Returning false here means we won't inspect the children of an ignored field.
return false
}

func checkField(pass *analysis.Pass, field *ast.Field, tagInfo extractjsontags.FieldTagInfo) {
if tagInfo.Name == "" {
return true
return
}

fieldName := field.Names[0].Name
var fieldName string
if len(field.Names) > 0 {
fieldName = field.Names[0].Name
} else if ident, ok := field.Type.(*ast.Ident); ok {
fieldName = ident.Name
}

if field.Doc == nil {
pass.Reportf(field.Pos(), "field %s is missing godoc comment", fieldName)
return true
return
}

firstLine := field.Doc.List[0]
Expand All @@ -125,6 +82,4 @@ func checkField(pass *analysis.Pass, field *ast.Field, jsonTags extractjsontags.
pass.Reportf(field.Doc.List[0].Pos(), "godoc for field %s should start with '%s ...'", fieldName, tagInfo.Name)
}
}

return true
}
10 changes: 10 additions & 0 deletions pkg/analysis/commentstart/testdata/src/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type CommentStartTestStruct struct {

StructForInlineField `json:",inline"`

A `json:"a"` // want "field A is missing godoc comment"

// IncorrectStartComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectStartComment should start with 'incorrectStartComment ...'"
IncorrectStartComment string `json:"incorrectStartComment"`

Expand Down Expand Up @@ -53,6 +55,10 @@ type StructForInlineField struct {
NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment"
}

type A struct {
NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment"
}

type unexportedStruct struct {
NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment"
}
Expand All @@ -71,3 +77,7 @@ func FunctionWithStructs() {
NoComment string `json:"noComment"`
}
}

type Interface interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need to test a type that uses composition of the interface like:

type ComposedThing struct {
        Interface
        Foo string
}

?

Copy link
Contributor Author

@JoelSpeed JoelSpeed Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can embed interfaces like that, you can embed other structs, not interfaces, I thought we had that case already though

Apparently you can

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would just mean you have to initialise the interface, inside the struct, with something that implements the interface.

At the moment I can't see why anyone would be using that inside a Kube API type, but maybe there are other types in the same package that this might effect?

For now I'm tempted to leave this out until we see a use case

InaccessibleFunction() string
}
10 changes: 10 additions & 0 deletions pkg/analysis/commentstart/testdata/src/a/a.go.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type CommentStartTestStruct struct {

StructForInlineField `json:",inline"`

A `json:"a"` // want "field A is missing godoc comment"

// incorrectStartComment is a field with an incorrect start to the comment. // want "godoc for field IncorrectStartComment should start with 'incorrectStartComment ...'"
IncorrectStartComment string `json:"incorrectStartComment"`

Expand Down Expand Up @@ -53,6 +55,10 @@ type StructForInlineField struct {
NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment"
}

type A struct {
NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment"
}

type unexportedStruct struct {
NoComment string `json:"noComment"` // want "field NoComment is missing godoc comment"
}
Expand All @@ -71,3 +77,7 @@ func FunctionWithStructs() {
NoComment string `json:"noComment"`
}
}

type Interface interface {
InaccessibleFunction() string
}
51 changes: 51 additions & 0 deletions pkg/analysis/helpers/inspector/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package inspector

import (
"errors"
"reflect"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
astinspector "golang.org/x/tools/go/ast/inspector"

"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
)

const name = "inspector"

var (
errCouldNotGetInspector = errors.New("could not get inspector")
errCouldNotGetJSONTags = errors.New("could not get json tags")
errCouldNotGetMarkers = errors.New("could not get markers")
)

// Analyzer is the analyzer for the inspector package.
// It provides common functionality for analyzers that need to inspect fields and struct.
// Abstracting away filtering of fields that the analyzers should and shouldn't be worrying about.
var Analyzer = &analysis.Analyzer{
Name: name,
Doc: "Provides common functionality for analyzers that need to inspect fields and struct",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, extractjsontags.Analyzer, markers.Analyzer},
ResultType: reflect.TypeOf(newInspector(nil, nil, nil)),
}

func run(pass *analysis.Pass) (interface{}, error) {
astinspector, ok := pass.ResultOf[inspect.Analyzer].(*astinspector.Inspector)
if !ok {
return nil, errCouldNotGetInspector
}

jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags)
if !ok {
return nil, errCouldNotGetJSONTags
}

markersAccess, ok := pass.ResultOf[markers.Analyzer].(markers.Markers)
if !ok {
return nil, errCouldNotGetMarkers
}

return newInspector(astinspector, jsonTags, markersAccess), nil
}
52 changes: 52 additions & 0 deletions pkg/analysis/helpers/inspector/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package inspector_test

import (
"errors"
"go/ast"
"testing"

"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
"github.com/JoelSpeed/kal/pkg/analysis/helpers/inspector"
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/analysistest"
)

func TestInspector(t *testing.T) {
testdata := analysistest.TestData()

analysistest.Run(t, testdata, testAnalyzer, "a")
}

var errCouldNotGetInspector = errors.New("could not get inspector")

var testAnalyzer = &analysis.Analyzer{
Name: "test",
Doc: "tests the inspector analyzer",
Run: run,
Requires: []*analysis.Analyzer{inspector.Analyzer},
}

func run(pass *analysis.Pass) (interface{}, error) {
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
if !ok {
return nil, errCouldNotGetInspector
}

inspect.InspectFields(func(field *ast.Field, stack []ast.Node, jsonTagInfo extractjsontags.FieldTagInfo, markersAccess markers.Markers) {
var fieldName string
if len(field.Names) > 0 {
fieldName = field.Names[0].Name
} else if ident, ok := field.Type.(*ast.Ident); ok {
fieldName = ident.Name
}

pass.Reportf(field.Pos(), "field: %v", fieldName)

if jsonTagInfo.Name != "" {
pass.Reportf(field.Pos(), "json tag: %v", jsonTagInfo.Name)
}
})

return nil, nil //nolint:nilnil
}
34 changes: 34 additions & 0 deletions pkg/analysis/helpers/inspector/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
inspector is a helper package that iterates over fields in structs, calling an inspection function on fields
that should be considered for analysis.

The inspector extracts common logic of iterating and filtering through struct fields, so that analyzers
need not re-implement the same filtering over and over.

For example, the inspector filters out struct definitions that are not type declarations, and fields that are ignored.

Example:

type A struct {
// This field is included in the analysis.
Field string `json:"field"`

// This field, and the fields within are ignored due to the json tag.
F struct {
Field string `json:"field"`
} `json:"-"`
}

// Any struct defined within a function is ignored.
func Foo() {
type Bar struct {
Field string
}
}

// All fields within interface declarations are ignored.
type Bar interface {
Name() string
}
*/
package inspector
Loading