Skip to content

Adds nophase linter #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 11, 2024
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ lintersConfig:
jsonTagRegex: "^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$" # Provide a custom regex, which the json tag must match.
```

## Nophase

The `nophase` linter checks that the fields in the API types don't contain a 'Phase', or any field which contains 'Phase' as a substring, e.g MachinePhase.

## OptionalOrRequired

The `optionalorrequired` linter checks that all fields in the API types are either optional or required, and are marked explicitly as such.
Expand Down
4 changes: 3 additions & 1 deletion docs/new-linter.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ Basic tests can be implemented with the Go analysis test framework.
Create a `testdata` directory in the linter package and create a structure underneath.
Individual test files must be placed under `src` and then a subdirectory for each package.

Use one package per configuration input for the linter.
If your linter has different configurations, e.g options to pass to the linter
you will need one configuration per option. Use one package per configuration
input for the linter.

```
mylinter
Expand Down
91 changes: 91 additions & 0 deletions pkg/analysis/nophase/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package nophase

import (
"errors"
"go/ast"
"strings"

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

const name = "nophase"

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

// Analyzer is the analyzer for the nophase package.
// It checks that no struct fields named 'phase', or that contain phase as a
// substring are present.
var Analyzer = &analysis.Analyzer{
Name: name,
Doc: "phase fields are deprecated and conditions should be preferred, avoid phase like enum fields",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, extractjsontags.Analyzer},
}

func run(pass *analysis.Pass) (interface{}, error) {
inspect, ok := pass.ResultOf[inspect.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.StructType)(nil),
}

// Preorder visits all the nodes of the AST in depth-first order. It calls
// f(n) for each node n before it visits n's children.
//
// We use the filter defined above, ensuring we only look at struct fields.
inspect.Preorder(nodeFilter, func(n ast.Node) {
sTyp, ok := n.(*ast.StructType)
if !ok {
return
}

if sTyp.Fields == nil {
return
}

for _, field := range sTyp.Fields.List {
if field == nil || len(field.Names) == 0 {
continue
}

fieldName := field.Names[0].Name

// First check if the struct field name contains 'phase'
if strings.Contains(strings.ToLower(fieldName), "phase") {
pass.Reportf(field.Pos(),
"field %s: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields",
fieldName,
)

continue
}

// Then check if the json serialization of the field contains 'phase'
tagInfo := jsonTags.FieldTags(sTyp, fieldName)

if strings.Contains(strings.ToLower(tagInfo.Name), "phase") {
pass.Reportf(field.Pos(),
"field %s: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields",
fieldName,
)
}
}
})

return nil, nil //nolint:nilnil
}
13 changes: 13 additions & 0 deletions pkg/analysis/nophase/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package nophase_test

import (
"testing"

"github.com/JoelSpeed/kal/pkg/analysis/nophase"
"golang.org/x/tools/go/analysis/analysistest"
)

func Test(t *testing.T) {
testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, nophase.Analyzer, "a")
}
11 changes: 11 additions & 0 deletions pkg/analysis/nophase/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
nophase provides a linter to ensure that structs do not contain a Phase field.

Phase fields are deprecated and conditions should be preferred. Avoid phase like enum
fields.

The linter will flag any struct field containing the substring 'phase'. This means both
Phase and FooPhase will be flagged.
*/

package nophase
30 changes: 30 additions & 0 deletions pkg/analysis/nophase/initializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package nophase

import (
"github.com/JoelSpeed/kal/pkg/config"
"golang.org/x/tools/go/analysis"
)

// Initializer returns the AnalyzerInitializer for this
// Analyzer so that it can be added to the registry.
func Initializer() initializer {
return initializer{}
}

// intializer implements the AnalyzerInitializer interface.
type initializer struct{}

// Name returns the name of the Analyzer.
func (initializer) Name() string {
return name
}

// Init returns the intialized Analyzer.
func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) {
return Analyzer, nil
}

// Default determines whether this Analyzer is on by default, or not.
func (initializer) Default() bool {
return true
}
19 changes: 19 additions & 0 deletions pkg/analysis/nophase/testdata/src/a/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package a

type NoPhaseTestStruct struct {
// +optional
Phase *string `json:"phase,omitempty"` // want "field Phase: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields"

}

type NoSubPhaseTestStruct struct {
// +optional
FooPhase *string `json:"fooPhase,omitempty"` // want "field FooPhase: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields"

}

type SerializedPhaseTeststruct struct {
// +optional
FooField *string `json:"fooPhase,omitempty"` // want "field FooField: phase fields are deprecated and conditions should be preferred, avoid phase like enum fields"

}
2 changes: 2 additions & 0 deletions pkg/analysis/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/JoelSpeed/kal/pkg/analysis/commentstart"
"github.com/JoelSpeed/kal/pkg/analysis/jsontags"
"github.com/JoelSpeed/kal/pkg/analysis/nophase"
"github.com/JoelSpeed/kal/pkg/analysis/optionalorrequired"
"github.com/JoelSpeed/kal/pkg/analysis/requiredfields"
"github.com/JoelSpeed/kal/pkg/config"
Expand Down Expand Up @@ -51,6 +52,7 @@ func NewRegistry() Registry {
initializers: []AnalyzerInitializer{
commentstart.Initializer(),
jsontags.Initializer(),
nophase.Initializer(),
optionalorrequired.Initializer(),
requiredfields.Initializer(),
},
Expand Down
2 changes: 2 additions & 0 deletions pkg/analysis/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var _ = Describe("Registry", func() {
Expect(r.DefaultLinters().UnsortedList()).To(ConsistOf(
"commentstart",
"jsontags",
"nophase",
"optionalorrequired",
"requiredfields",
))
Expand All @@ -30,6 +31,7 @@ var _ = Describe("Registry", func() {
Expect(r.AllLinters().UnsortedList()).To(ConsistOf(
"commentstart",
"jsontags",
"nophase",
"optionalorrequired",
"requiredfields",
))
Expand Down
Loading