package main import ( "bufio" "os" "path/filepath" "regexp" "strings" "git.dev.alexdunmow.com/block/check-safety/internal/helpers" ) // --- Check: No useState for tab state in route files --- type tabStateViolation struct { file string line int snippet string } type eslintDirectiveViolation struct { file string line int snippet string } var reUseStateTab = regexp.MustCompile(`useState\s*[<(].*(?i:tab|activeTab|selectedTab|currentTab)`) var reUseStateTabAlt = regexp.MustCompile(`(?:activeTab|selectedTab|currentTab|tabValue)\s*,\s*set\w+\]\s*=\s*useState`) // False positives: layout preferences that are not content tabs var tabStateIgnorePatterns = []string{"preferTabs"} var reESLintDisableNextLine = regexp.MustCompile(`eslint-disable-next-line`) func checkTabState(webSrcDir string) []tabStateViolation { var violations []tabStateViolation if err := filepath.Walk(webSrcDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if !strings.HasSuffix(path, ".tsx") { return nil } if strings.Contains(path, "node_modules") || strings.Contains(path, "/dist/") || strings.Contains(path, "/.worktrees/") { return nil } relPath, _ := filepath.Rel(webSrcDir, path) relPath = filepath.ToSlash(relPath) if !strings.HasPrefix(relPath, "routes/") { return nil } f, err := os.Open(path) if err != nil { return nil } defer helpers.LogDeferredError(nil, "close routable tab scan file", f.Close, "path", path) scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "//") { continue } isIgnored := false for _, pat := range tabStateIgnorePatterns { if strings.Contains(line, pat) { isIgnored = true break } } if !isIgnored && (reUseStateTab.MatchString(line) || reUseStateTabAlt.MatchString(line)) { violations = append(violations, tabStateViolation{ file: relPath, line: lineNum, snippet: strings.TrimSpace(line), }) } } return nil }); err != nil { violations = append(violations, tabStateViolation{ file: filepath.ToSlash(webSrcDir), line: 0, snippet: err.Error(), }) } return violations } func checkESLintDisableNextLine(root string) []eslintDirectiveViolation { var violations []eslintDirectiveViolation if err := 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" && ext != ".js" && ext != ".jsx" { return nil } if strings.Contains(path, "node_modules") || strings.Contains(path, "/dist/") || strings.Contains(path, "/.worktrees/") { return nil } lines, readErr := readSourceLines(path) if readErr != nil { return nil } relPath, _ := filepath.Rel(root, path) relPath = filepath.ToSlash(relPath) if relPath == "" { relPath = filepath.ToSlash(path) } for idx, line := range lines { if !reESLintDisableNextLine.MatchString(line) { continue } violations = append(violations, eslintDirectiveViolation{ file: relPath, line: idx + 1, snippet: strings.TrimSpace(line), }) } return nil }); err != nil { violations = append(violations, eslintDirectiveViolation{ file: filepath.ToSlash(root), line: 0, snippet: err.Error(), }) } return violations } // --- Check: Import from @block-ninja/api subpaths only --- type bareImportViolation struct { file string line int snippet string } // Matches: from '@block-ninja/api' (bare, without /types, /queries, /services suffix) var reBareAPIImport = regexp.MustCompile(`from\s+['"]@block-ninja/api['"]`) func checkBareImports(webSrcDir string) []bareImportViolation { var violations []bareImportViolation if err := filepath.Walk(webSrcDir, 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, "/.worktrees/") { return nil } relPath, _ := filepath.Rel(webSrcDir, path) f, err := os.Open(path) if err != nil { return nil } defer helpers.LogDeferredError(nil, "close bare API import scan file", f.Close, "path", path) scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() if reBareAPIImport.MatchString(line) { violations = append(violations, bareImportViolation{ file: relPath, line: lineNum, snippet: strings.TrimSpace(line), }) } } return nil }); err != nil { violations = append(violations, bareImportViolation{ file: filepath.ToSlash(webSrcDir), line: 0, snippet: err.Error(), }) } return violations } // --- Check: No npm/yarn lockfiles (pnpm only) --- type lockfileViolation struct { file string } // allowedLockfiles are non-pnpm lockfiles intentionally tolerated, keyed by // repo-relative slash path. These live in gitignored tool working dirs (never // committed) and are regenerated by an external toolchain we don't control, so // they are not part of the repo's dependency surface. var allowedLockfiles = map[string]bool{ // cms/styles is a gitignored tailwind-service working dir; its // package-lock.json is regenerated by the tailwind tooling and never // committed (see cms/.gitignore). "styles/package-lock.json": true, } func checkLockfiles(repoRoot string) []lockfileViolation { var violations []lockfileViolation if err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if strings.Contains(path, "node_modules") || strings.Contains(path, "/.worktrees/") { return filepath.SkipDir } base := filepath.Base(path) if base == "package-lock.json" || base == "yarn.lock" { relPath, _ := filepath.Rel(repoRoot, path) rel := filepath.ToSlash(relPath) if allowedLockfiles[rel] { return nil } violations = append(violations, lockfileViolation{file: rel}) } return nil }); err != nil { violations = append(violations, lockfileViolation{file: filepath.ToSlash(repoRoot)}) } return violations }