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 }