-
Notifications
You must be signed in to change notification settings - Fork 468
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into add-brotlilevel-methods
- Loading branch information
Showing
3 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"go/ast" | ||
"go/token" | ||
"reflect" | ||
"strings" | ||
|
||
"golang.org/x/tools/go/analysis" | ||
"golang.org/x/tools/go/analysis/singlechecker" | ||
) | ||
|
||
// Tip for linter that struct that has this comment should be included in the | ||
// analysis. | ||
// Note: comment should be directly line above the struct definition. | ||
const linterTip = "// lint:require-exhaustive-initialization" | ||
|
||
func New(conf any) ([]*analysis.Analyzer, error) { | ||
return []*analysis.Analyzer{Analyzer}, nil | ||
} | ||
|
||
// Analyzer implements struct analyzer for structs that are annotated with | ||
// `linterTip`, it checks that every instantiation initializes all the fields. | ||
var Analyzer = &analysis.Analyzer{ | ||
Name: "structinit", | ||
Doc: "check for struct field initializations", | ||
Run: func(p *analysis.Pass) (interface{}, error) { return run(false, p) }, | ||
ResultType: reflect.TypeOf(Result{}), | ||
} | ||
|
||
var analyzerForTests = &analysis.Analyzer{ | ||
Name: "teststructinit", | ||
Doc: "check for struct field initializations", | ||
Run: func(p *analysis.Pass) (interface{}, error) { return run(true, p) }, | ||
ResultType: reflect.TypeOf(Result{}), | ||
} | ||
|
||
type structError struct { | ||
Pos token.Pos | ||
Message string | ||
} | ||
|
||
type Result struct { | ||
Errors []structError | ||
} | ||
|
||
func run(dryRun bool, pass *analysis.Pass) (interface{}, error) { | ||
var ( | ||
ret Result | ||
structs = markedStructs(pass) | ||
) | ||
for _, f := range pass.Files { | ||
ast.Inspect(f, func(node ast.Node) bool { | ||
// For every composite literal check that number of elements in | ||
// the literal match the number of struct fields. | ||
if cl, ok := node.(*ast.CompositeLit); ok { | ||
stName := pass.TypesInfo.Types[cl].Type.String() | ||
if cnt, found := structs[stName]; found && cnt != len(cl.Elts) { | ||
ret.Errors = append(ret.Errors, structError{ | ||
Pos: cl.Pos(), | ||
Message: fmt.Sprintf("struct: %q initialized with: %v of total: %v fields", stName, len(cl.Elts), cnt), | ||
}) | ||
|
||
} | ||
|
||
} | ||
return true | ||
}) | ||
} | ||
for _, err := range ret.Errors { | ||
if !dryRun { | ||
pass.Report(analysis.Diagnostic{ | ||
Pos: err.Pos, | ||
Message: err.Message, | ||
Category: "structinit", | ||
}) | ||
} | ||
} | ||
return ret, nil | ||
} | ||
|
||
// markedStructs returns a map of structs that are annotated for linter to check | ||
// that all fields are initialized when the struct is instantiated. | ||
// It maps struct full name (including package path) to number of fields it contains. | ||
func markedStructs(pass *analysis.Pass) map[string]int { | ||
res := make(map[string]int) | ||
for _, f := range pass.Files { | ||
tips := make(map[position]bool) | ||
ast.Inspect(f, func(node ast.Node) bool { | ||
switch n := node.(type) { | ||
case *ast.Comment: | ||
p := pass.Fset.Position(node.Pos()) | ||
if strings.Contains(n.Text, linterTip) { | ||
tips[position{p.Filename, p.Line + 1}] = true | ||
} | ||
case *ast.TypeSpec: | ||
if st, ok := n.Type.(*ast.StructType); ok { | ||
p := pass.Fset.Position(st.Struct) | ||
if tips[position{p.Filename, p.Line}] { | ||
fieldsCnt := 0 | ||
for _, field := range st.Fields.List { | ||
fieldsCnt += len(field.Names) | ||
} | ||
res[pass.Pkg.Path()+"."+n.Name.Name] = fieldsCnt | ||
} | ||
} | ||
} | ||
return true | ||
}) | ||
} | ||
return res | ||
} | ||
|
||
type position struct { | ||
fileName string | ||
line int | ||
} | ||
|
||
func main() { | ||
singlechecker.Main(Analyzer) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package main | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"golang.org/x/tools/go/analysis/analysistest" | ||
) | ||
|
||
func testData(t *testing.T) string { | ||
t.Helper() | ||
wd, err := os.Getwd() | ||
if err != nil { | ||
t.Fatalf("Failed to get wd: %s", err) | ||
} | ||
return filepath.Join(filepath.Dir(wd), "testdata") | ||
} | ||
|
||
func TestLinter(t *testing.T) { | ||
testdata := testData(t) | ||
got := errCount(analysistest.Run(t, testdata, analyzerForTests, "structinit/a")) | ||
if got != 2 { | ||
t.Errorf("analysistest.Run() got %d errors, expected 2", got) | ||
} | ||
} | ||
|
||
func errCount(res []*analysistest.Result) int { | ||
cnt := 0 | ||
for _, r := range res { | ||
if rs, ok := r.Result.(Result); ok { | ||
cnt += len(rs.Errors) | ||
} | ||
} | ||
return cnt | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package a | ||
|
||
import "fmt" | ||
|
||
// lint:require-exhaustive-initialization | ||
type interestingStruct struct { | ||
x int | ||
b *boringStruct | ||
} | ||
|
||
type boringStruct struct { | ||
x, y int | ||
} | ||
|
||
func init() { | ||
a := &interestingStruct{ // Error: only single field is initialized. | ||
x: 1, | ||
} | ||
fmt.Println(a) | ||
b := interestingStruct{ // Error: only single field is initialized. | ||
b: nil, | ||
} | ||
fmt.Println(b) | ||
c := interestingStruct{ // Not an error, all fields are initialized. | ||
x: 1, | ||
b: nil, | ||
} | ||
fmt.Println(c) | ||
d := &boringStruct{ // Not an error since it's not annotated for the linter. | ||
x: 1, | ||
} | ||
fmt.Println(d) | ||
} |