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 }