Skip to content
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

move Visitor to a separate structure #82

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
192 changes: 6 additions & 186 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package analyzer

import (
"flag"
"fmt"
"go/ast"
"go/token"
"go/types"
"sync"

Expand Down Expand Up @@ -74,195 +72,17 @@ Anonymous structs can be matched by '<anonymous>' alias.
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert

visitor := &Visitor{
analyzer: a,
pass: pass,
}

insp.WithStack(
[]ast.Node{
(*ast.CompositeLit)(nil),
},
a.newVisitor(pass),
visitor.Visit,
)

return nil, nil //nolint:nilnil
}

// newVisitor returns visitor that only expects [ast.CompositeLit] nodes.
func (a *analyzer) newVisitor(pass *analysis.Pass) func(n ast.Node, push bool, stack []ast.Node) bool {
return func(n ast.Node, push bool, stack []ast.Node) bool {
if !push {
return true
}

lit, ok := n.(*ast.CompositeLit)
if !ok {
// this should never happen, but better be prepared
return true
}

structTyp, typeInfo, ok := getStructType(pass, lit)
if !ok {
return true
}

if len(lit.Elts) == 0 {
if ret, ok := stackParentIsReturn(stack); ok {
if returnContainsNonNilError(pass, ret) {
// it is okay to return uninitialized structure in case struct's direct parent is
// a return statement containing non-nil error
//
// we're unable to check if returned error is custom, but at least we're able to
// cover str [error] type.
return true
}
}
}

pos, msg := a.processStruct(pass, lit, structTyp, typeInfo)
if pos != nil {
pass.Reportf(*pos, msg)
}

return true
}
}

func getStructType(pass *analysis.Pass, lit *ast.CompositeLit) (*types.Struct, *TypeInfo, bool) {
switch typ := pass.TypesInfo.TypeOf(lit).(type) {
case *types.Named: // named type
if structTyp, ok := typ.Underlying().(*types.Struct); ok {
pkg := typ.Obj().Pkg()
ti := TypeInfo{
Name: typ.Obj().Name(),
PackageName: pkg.Name(),
PackagePath: pkg.Path(),
}

return structTyp, &ti, true
}

return nil, nil, false

case *types.Struct: // anonymous struct
ti := TypeInfo{
Name: "<anonymous>",
PackageName: pass.Pkg.Name(),
PackagePath: pass.Pkg.Path(),
}

return typ, &ti, true

default:
return nil, nil, false
}
}

func stackParentIsReturn(stack []ast.Node) (*ast.ReturnStmt, bool) {
// it is safe to skip boundary check, since stack always has at least one element
// - whole file.
ret, ok := stack[len(stack)-2].(*ast.ReturnStmt)

return ret, ok
}

func returnContainsNonNilError(pass *analysis.Pass, ret *ast.ReturnStmt) bool {
// errors are mostly located at the end of return statement, so we're starting
// from the end.
for i := len(ret.Results) - 1; i >= 0; i-- {
if pass.TypesInfo.TypeOf(ret.Results[i]).String() == "error" {
return true
}
}

return false
}

func (a *analyzer) processStruct(
pass *analysis.Pass,
lit *ast.CompositeLit,
structTyp *types.Struct,
info *TypeInfo,
) (*token.Pos, string) {
if !a.shouldProcessType(info) {
return nil, ""
}

// unnamed structures are only defined in same package, along with types that has
// prefix identical to current package name.
isSamePackage := info.PackagePath == pass.Pkg.Path()

if f := a.litSkippedFields(lit, structTyp, !isSamePackage); len(f) > 0 {
pos := lit.Pos()

if len(f) == 1 {
return &pos, fmt.Sprintf("%s is missing field %s", info.ShortString(), f.String())
}

return &pos, fmt.Sprintf("%s is missing fields %s", info.ShortString(), f.String())
}

return nil, ""
}

// shouldProcessType returns true if type should be processed basing off include
// and exclude patterns, defined though constructor and\or flags.
func (a *analyzer) shouldProcessType(info *TypeInfo) bool {
if len(a.include) == 0 && len(a.exclude) == 0 {
return true
}

name := info.String()

a.typeProcessingNeedMu.RLock()
res, ok := a.typeProcessingNeed[name]
a.typeProcessingNeedMu.RUnlock()

if !ok {
a.typeProcessingNeedMu.Lock()
res = true

if a.include != nil && !a.include.MatchFullString(name) {
res = false
}

if res && a.exclude != nil && a.exclude.MatchFullString(name) {
res = false
}

a.typeProcessingNeed[name] = res
a.typeProcessingNeedMu.Unlock()
}

return res
}

//revive:disable-next-line:unused-receiver
func (a *analyzer) litSkippedFields(
lit *ast.CompositeLit,
typ *types.Struct,
onlyExported bool,
) fields.StructFields {
a.fieldsCacheMu.RLock()
f, ok := a.fieldsCache[typ]
a.fieldsCacheMu.RUnlock()

if !ok {
a.fieldsCacheMu.Lock()
f = fields.NewStructFields(typ)
a.fieldsCache[typ] = f
a.fieldsCacheMu.Unlock()
}

return f.SkippedFields(lit, onlyExported)
}

type TypeInfo struct {
Name string
PackageName string
PackagePath string
}

func (t TypeInfo) String() string {
return t.PackagePath + "." + t.Name
}

func (t TypeInfo) ShortString() string {
return t.PackageName + "." + t.Name
}
Loading