Reporter is now a collector: checks declare a verdict (OK/Skip/Warn/Fail/ Fatal) plus findings, and a single central render() prints output. Default output is silent on pass/skip — only FAIL/WARN/ERR checks print, followed by one tally line, so a clean run is two lines. --verbose restores full per-check output. All ~30 checks were converted to this API; orphaned guidance/label helpers (printPerTargetOKLines, per-check *Help blocks, colors_format.go) were removed. The any-usage check (2e) now defaults to only the unstaged working-tree diff (changed lines), via a new per-repo git-diff index in changedlines.go; --all-any restores the full scan. Not-a-git-repo / no-diff warns on nothing. Golden fixtures regenerated; integration tests updated to the new format; added unit tests for the diff index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type anyUsageWarning struct {
|
|
file string
|
|
line int
|
|
lang string
|
|
snippet string
|
|
}
|
|
|
|
// lineKeeper decides whether an `any` at absPath:line should be reported. A nil
|
|
// keeper keeps everything (the --all-any full scan); the diff-scoped default
|
|
// keeps only lines present in the unstaged working-tree diff.
|
|
type lineKeeper func(absPath string, line int) bool
|
|
|
|
func (k lineKeeper) keep(absPath string, line int) bool {
|
|
return k == nil || k(absPath, line)
|
|
}
|
|
|
|
var (
|
|
reTypeScriptAny = regexp.MustCompile(`\bany\b`)
|
|
reTypeScriptAsAny = regexp.MustCompile(`\bas\s+any\b`)
|
|
reTypeScriptColonAny = regexp.MustCompile(`:\s*any(?:\b|[\s,\]\)>|=])`)
|
|
reTypeScriptCastAny = regexp.MustCompile(`<\s*any\s*>`)
|
|
reTypeScriptArrayAny = regexp.MustCompile(`\bany\s*\[\]`)
|
|
reTypeScriptEqAny = regexp.MustCompile(`=\s*any(?:\b|[\s;])`)
|
|
)
|
|
|
|
func checkGoAnyUsage(root string, keep lineKeeper) []anyUsageWarning {
|
|
var warnings []anyUsageWarning
|
|
|
|
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
|
return nil
|
|
}
|
|
if strings.Contains(path, "/vendor/") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(path, ".pb.go") || strings.HasSuffix(path, ".connect.go") || strings.HasSuffix(path, ".gen.go") || strings.HasSuffix(path, "_templ.go") {
|
|
return nil
|
|
}
|
|
|
|
lines, readErr := readSourceLines(path)
|
|
if readErr != nil {
|
|
return nil
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
file, parseErr := parser.ParseFile(fset, path, nil, 0)
|
|
if parseErr != nil {
|
|
return nil
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(root, path)
|
|
relPath = filepath.ToSlash(relPath)
|
|
seen := make(map[int]bool)
|
|
recordAnyExpr := func(expr ast.Expr) {
|
|
for _, pos := range findAnyTypePositions(expr) {
|
|
position := fset.Position(pos)
|
|
if seen[position.Line] {
|
|
continue
|
|
}
|
|
seen[position.Line] = true
|
|
if !keep.keep(path, position.Line) {
|
|
continue
|
|
}
|
|
snippet := ""
|
|
if position.Line-1 >= 0 && position.Line-1 < len(lines) {
|
|
snippet = strings.TrimSpace(lines[position.Line-1])
|
|
}
|
|
warnings = append(warnings, anyUsageWarning{
|
|
file: relPath,
|
|
line: position.Line,
|
|
lang: "go",
|
|
snippet: snippet,
|
|
})
|
|
}
|
|
}
|
|
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
switch node := n.(type) {
|
|
case *ast.Field:
|
|
recordAnyExpr(node.Type)
|
|
case *ast.TypeSpec:
|
|
recordAnyExpr(node.Type)
|
|
case *ast.ValueSpec:
|
|
recordAnyExpr(node.Type)
|
|
case *ast.FuncDecl:
|
|
if node.Type != nil {
|
|
recordAnyExpr(node.Type)
|
|
}
|
|
case *ast.FuncLit:
|
|
if node.Type != nil {
|
|
recordAnyExpr(node.Type)
|
|
}
|
|
case *ast.TypeAssertExpr:
|
|
recordAnyExpr(node.Type)
|
|
case *ast.CompositeLit:
|
|
recordAnyExpr(node.Type)
|
|
}
|
|
return true
|
|
})
|
|
|
|
return nil
|
|
})
|
|
|
|
return warnings
|
|
}
|
|
|
|
func findAnyTypePositions(expr ast.Expr) []token.Pos {
|
|
if expr == nil {
|
|
return nil
|
|
}
|
|
|
|
var positions []token.Pos
|
|
switch node := expr.(type) {
|
|
case *ast.Ident:
|
|
if node.Name == "any" {
|
|
positions = append(positions, node.Pos())
|
|
}
|
|
case *ast.ArrayType:
|
|
positions = append(positions, findAnyTypePositions(node.Elt)...)
|
|
case *ast.MapType:
|
|
positions = append(positions, findAnyTypePositions(node.Key)...)
|
|
positions = append(positions, findAnyTypePositions(node.Value)...)
|
|
case *ast.ChanType:
|
|
positions = append(positions, findAnyTypePositions(node.Value)...)
|
|
case *ast.Ellipsis:
|
|
positions = append(positions, findAnyTypePositions(node.Elt)...)
|
|
case *ast.ParenExpr:
|
|
positions = append(positions, findAnyTypePositions(node.X)...)
|
|
case *ast.StarExpr:
|
|
positions = append(positions, findAnyTypePositions(node.X)...)
|
|
case *ast.IndexExpr:
|
|
positions = append(positions, findAnyTypePositions(node.X)...)
|
|
positions = append(positions, findAnyTypePositions(node.Index)...)
|
|
case *ast.IndexListExpr:
|
|
positions = append(positions, findAnyTypePositions(node.X)...)
|
|
for _, index := range node.Indices {
|
|
positions = append(positions, findAnyTypePositions(index)...)
|
|
}
|
|
case *ast.SelectorExpr:
|
|
positions = append(positions, findAnyTypePositions(node.X)...)
|
|
case *ast.StructType:
|
|
if node.Fields != nil {
|
|
for _, field := range node.Fields.List {
|
|
positions = append(positions, findAnyTypePositions(field.Type)...)
|
|
}
|
|
}
|
|
case *ast.InterfaceType:
|
|
if node.Methods != nil {
|
|
for _, field := range node.Methods.List {
|
|
positions = append(positions, findAnyTypePositions(field.Type)...)
|
|
}
|
|
}
|
|
case *ast.FuncType:
|
|
if node.Params != nil {
|
|
for _, field := range node.Params.List {
|
|
positions = append(positions, findAnyTypePositions(field.Type)...)
|
|
}
|
|
}
|
|
if node.Results != nil {
|
|
for _, field := range node.Results.List {
|
|
positions = append(positions, findAnyTypePositions(field.Type)...)
|
|
}
|
|
}
|
|
}
|
|
|
|
return positions
|
|
}
|
|
|
|
func checkTypeScriptAnyUsage(root string, keep lineKeeper) []anyUsageWarning {
|
|
var warnings []anyUsageWarning
|
|
|
|
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return nil
|
|
}
|
|
ext := filepath.Ext(path)
|
|
if ext != ".ts" && ext != ".tsx" {
|
|
return nil
|
|
}
|
|
if strings.Contains(path, "node_modules") || strings.Contains(path, "/dist/") || strings.Contains(path, "/generated/") {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(path, ".d.ts") || strings.HasSuffix(path, ".gen.ts") || strings.HasSuffix(path, ".gen.tsx") {
|
|
return nil
|
|
}
|
|
|
|
lines, readErr := readSourceLines(path)
|
|
if readErr != nil {
|
|
return nil
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(root, path)
|
|
relPath = filepath.ToSlash(relPath)
|
|
for idx, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") {
|
|
continue
|
|
}
|
|
if strings.Contains(line, "'any'") || strings.Contains(line, `"any"`) || strings.Contains(line, "`any") || strings.Contains(line, "any`") {
|
|
continue
|
|
}
|
|
if !reTypeScriptAny.MatchString(line) {
|
|
continue
|
|
}
|
|
if !reTypeScriptAsAny.MatchString(line) && !reTypeScriptColonAny.MatchString(line) && !reTypeScriptCastAny.MatchString(line) && !reTypeScriptArrayAny.MatchString(line) && !reTypeScriptEqAny.MatchString(line) {
|
|
continue
|
|
}
|
|
|
|
warnings = append(warnings, anyUsageWarning{
|
|
file: relPath,
|
|
line: idx + 1,
|
|
lang: "ts",
|
|
snippet: trimmed,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return warnings
|
|
}
|