check-safety/lint.go
Alex Dunmow f968cb31d9 fix: skip gitignored .worktrees/ dirs in repo scans
Directory walkers pruned .git/vendor/node_modules but not .worktrees/, so a repo scan descended into nested git worktrees (e.g. an orchestrator worktree under cms/.worktrees/). Their Go/TS files surfaced as false positives in the standalone-plugin import check and noise in the any-usage warnings.

Add ".worktrees" to the skip set across the implicated and common walkers: proto RBAC proto-scan, standalone-plugin imports, frontend extras, go-lint, sqlc-uuid, presets.json, and plugin segmentation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:53:18 +08:00

266 lines
5.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
type goLintTarget struct {
label string
moduleRoot string
}
type goLintFailure struct {
target string
stage string
output string
}
type frontendLintTarget struct {
label string
packageRoot string
sourceDir string
sourceArg string
configPath string
pluginRules bool
managedConfigs bool
tsconfigPath string
hasTypeScript bool
}
type frontendLintFailure struct {
target string
stage string
output string
}
func discoverGoLintTargets(repoRoot string, backendDirs []string, pluginRoots []string) ([]goLintTarget, []string, error) {
var targets []goLintTarget
var skipped []string
seen := make(map[string]bool)
candidates := append(append([]string{}, backendDirs...), pluginRoots...)
for _, candidate := range candidates {
moduleRoot, ok := findNearestGoModuleRoot(candidate)
if !ok {
skipped = append(skipped, fmt.Sprintf("%s (no go.mod found)", displayLintPath(repoRoot, candidate)))
continue
}
if seen[moduleRoot] {
continue
}
seen[moduleRoot] = true
targets = append(targets, goLintTarget{
label: displayLintPath(repoRoot, moduleRoot),
moduleRoot: moduleRoot,
})
}
sort.Slice(targets, func(i, j int) bool {
return targets[i].label < targets[j].label
})
return targets, skipped, nil
}
func discoverFrontendLintTargets(repoRoot string, scanTargets []frontendScanTarget) ([]frontendLintTarget, []string, error) {
var targets []frontendLintTarget
var skipped []string
seen := make(map[string]bool)
for _, scanTarget := range scanTargets {
absCandidate, err := filepath.Abs(scanTarget.dir)
if err != nil {
return nil, nil, err
}
if seen[absCandidate] {
continue
}
seen[absCandidate] = true
info, err := os.Stat(absCandidate)
if err != nil || !info.IsDir() {
continue
}
packageRoot, ok := findNearestPackageRoot(absCandidate)
if !ok {
skipped = append(skipped, fmt.Sprintf("%s (no package.json found)", displayLintPath(repoRoot, absCandidate)))
continue
}
if scanTarget.pluginRules || scanTarget.managedConfigs {
if err := ensureDefaultFrontendConfigs(repoRoot, packageRoot); err != nil {
return nil, nil, err
}
}
configPath, ok := findNearestESLintConfig(absCandidate)
if !ok {
skipped = append(skipped, fmt.Sprintf("%s (no ESLint config found)", displayLintPath(repoRoot, absCandidate)))
continue
}
sourceArg, err := filepath.Rel(packageRoot, absCandidate)
if err != nil {
return nil, nil, err
}
if sourceArg == "" {
sourceArg = "."
}
hasTypeScript := dirContainsTypeScript(absCandidate)
tsconfigPath := ""
if hasTypeScript {
tsconfigPath, _ = findNearestTSConfig(packageRoot)
}
targets = append(targets, frontendLintTarget{
label: displayLintPath(repoRoot, absCandidate),
packageRoot: packageRoot,
sourceDir: absCandidate,
sourceArg: sourceArg,
configPath: configPath,
pluginRules: scanTarget.pluginRules,
managedConfigs: scanTarget.managedConfigs,
tsconfigPath: tsconfigPath,
hasTypeScript: hasTypeScript,
})
}
sort.Slice(targets, func(i, j int) bool {
return targets[i].label < targets[j].label
})
return targets, skipped, nil
}
func usesFlatESLintConfig(configPath string) bool {
base := filepath.Base(configPath)
return strings.HasPrefix(base, "eslint.config.")
}
func displayLintPath(repoRoot, path string) string {
if rel, err := filepath.Rel(repoRoot, path); err == nil && !strings.HasPrefix(rel, "..") {
return rel
}
return path
}
func findNearestPackageRoot(start string) (string, bool) {
for dir := start; ; dir = filepath.Dir(dir) {
info, err := os.Stat(filepath.Join(dir, "package.json"))
if err == nil && !info.IsDir() {
return dir, true
}
parent := filepath.Dir(dir)
if parent == dir {
return "", false
}
}
}
func findNearestGoModuleRoot(start string) (string, bool) {
for dir := start; ; dir = filepath.Dir(dir) {
info, err := os.Stat(filepath.Join(dir, "go.mod"))
if err == nil && !info.IsDir() {
return dir, true
}
parent := filepath.Dir(dir)
if parent == dir {
return "", false
}
}
}
func findNearestESLintConfig(start string) (string, bool) {
for dir := start; ; dir = filepath.Dir(dir) {
for _, name := range []string{
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
".eslintrc",
".eslintrc.js",
".eslintrc.cjs",
".eslintrc.json",
".eslintrc.yaml",
".eslintrc.yml",
} {
path := filepath.Join(dir, name)
info, err := os.Stat(path)
if err == nil && !info.IsDir() {
return path, true
}
}
if packageJSONHasESLintConfig(filepath.Join(dir, "package.json")) {
return filepath.Join(dir, "package.json"), true
}
parent := filepath.Dir(dir)
if parent == dir {
return "", false
}
}
}
func findNearestTSConfig(start string) (string, bool) {
for dir := start; ; dir = filepath.Dir(dir) {
for _, name := range []string{
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.build.json",
} {
path := filepath.Join(dir, name)
info, err := os.Stat(path)
if err == nil && !info.IsDir() {
return path, true
}
}
parent := filepath.Dir(dir)
if parent == dir {
return "", false
}
}
}
func dirContainsTypeScript(root string) bool {
found := false
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || found {
return nil
}
if info.IsDir() {
switch info.Name() {
case "node_modules", "dist", "build", ".git", ".worktrees":
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(path, ".ts") || strings.HasSuffix(path, ".tsx") {
found = true
}
return nil
})
return found
}
func packageJSONHasESLintConfig(path string) bool {
content, err := os.ReadFile(path)
if err != nil {
return false
}
var payload map[string]json.RawMessage
if err := json.Unmarshal(content, &payload); err != nil {
return false
}
_, ok := payload["eslintConfig"]
return ok
}