From 154adc3dfd9c2eadf3356b6a1e900486d0300289 Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Fri, 24 Jul 2026 18:14:58 -0400 Subject: [PATCH 1/4] fix: resolve TypeScript pnpm imports --- .../quality/quality_ai_script_resolution.go | 267 ++++++++++++++++++ .../quality/quality_ai_target_script.go | 50 +++- tests/checks/quality_ai_additional_test.go | 39 +++ 3 files changed, 341 insertions(+), 15 deletions(-) create mode 100644 internal/codeguard/checks/quality/quality_ai_script_resolution.go diff --git a/internal/codeguard/checks/quality/quality_ai_script_resolution.go b/internal/codeguard/checks/quality/quality_ai_script_resolution.go new file mode 100644 index 0000000..cab96f6 --- /dev/null +++ b/internal/codeguard/checks/quality/quality_ai_script_resolution.go @@ -0,0 +1,267 @@ +package quality + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "github.com/devr-tools/codeguard/internal/codeguard/checks/support" + "github.com/devr-tools/codeguard/internal/codeguard/core" +) + +// scriptTSConfigPathMap is deliberately limited to resolution-relevant +// tsconfig fields. The check only needs to establish whether an import is +// plausible, not type-check the program. +type scriptTSConfigPathMap struct { + dir string + baseURL string + paths map[string][]string +} + +type scriptTSConfigDocument struct { + CompilerOptions struct { + BaseURL string `json:"baseUrl"` + Paths map[string][]string `json:"paths"` + } `json:"compilerOptions"` +} + +func newScriptImportCatalog(env support.Context, target core.TargetConfig, rootManifest packageManifest) scriptImportCatalog { + catalog := scriptImportCatalog{ + deps: packageManifestDeps(rootManifest), + workspacePackage: map[string]struct{}{}, + packageManifests: map[string]packageManifest{}, + lockPackages: readPNPMLockPackages(target.Path), + } + if _, ok := readPackageManifest(target.Path); ok { + catalog.hasManifest = true + catalog.packageManifests["."] = rootManifest + } + for _, rel := range listAITargetFiles(env, target, func(rel string) bool { + base := filepath.Base(rel) + return base == "package.json" || strings.HasPrefix(base, "tsconfig") && strings.HasSuffix(base, ".json") + }) { + data, err := readAITargetFile(env, target, rel) + if err != nil { + continue + } + switch filepath.Base(rel) { + case "package.json": + manifest, ok := parsePackageManifest(data) + if !ok { + continue + } + catalog.hasManifest = true + dir := filepath.Clean(filepath.Dir(rel)) + catalog.packageManifests[dir] = manifest + if name := strings.TrimSpace(manifest.Name); name != "" { + catalog.workspacePackage[name] = struct{}{} + } + default: + var config scriptTSConfigDocument + if json.Unmarshal(stripScriptJSONC(data), &config) != nil || len(config.CompilerOptions.Paths) == 0 { + continue + } + dir := filepath.Clean(filepath.Dir(rel)) + baseURL := config.CompilerOptions.BaseURL + if baseURL == "" { + baseURL = "." + } + catalog.tsconfigPathMaps = append(catalog.tsconfigPathMaps, scriptTSConfigPathMap{dir: dir, baseURL: baseURL, paths: config.CompilerOptions.Paths}) + } + } + return catalog +} + +// readPNPMLockPackages extracts package roots from pnpm's lockfile without +// depending on a YAML parser. Both the current name@version format and the +// older /name/version format are supported. A lockfile is installation +// evidence, so it is a useful fallback when node_modules is absent in CI. +func readPNPMLockPackages(root string) map[string]struct{} { + data, err := os.ReadFile(filepath.Join(root, "pnpm-lock.yaml")) //nolint:gosec // fixed filename under scan target + if err != nil { + return map[string]struct{}{} + } + packages := map[string]struct{}{} + inPackages := false + for _, line := range strings.Split(string(data), "\n") { + if !strings.HasPrefix(line, " ") { + inPackages = strings.TrimSpace(line) == "packages:" + continue + } + if !inPackages || !strings.HasPrefix(line, " ") || strings.HasPrefix(line, " ") { + continue + } + key := strings.Trim(strings.TrimSuffix(strings.TrimSpace(line), ":"), "'\"") + if name := pnpmLockPackageName(key); name != "" { + packages[name] = struct{}{} + } + } + return packages +} + +func pnpmLockPackageName(key string) string { + key = strings.TrimPrefix(key, "/") + if key == "" || strings.HasPrefix(key, "#") { + return "" + } + if strings.HasPrefix(key, "@") { + if slash := strings.Index(key, "/"); slash > 1 { + if at := strings.Index(key[slash+1:], "@"); at >= 0 { + return key[:slash+1+at] + } + parts := strings.Split(key, "/") + if len(parts) >= 2 { + return parts[0] + "/" + parts[1] + } + } + return "" + } + if at := strings.Index(key, "@"); at > 0 { + return key[:at] + } + if slash := strings.Index(key, "/"); slash > 0 { + return key[:slash] + } + return "" +} + +func (catalog scriptImportCatalog) declaredDependenciesFor(file string) map[string]struct{} { + dir := filepath.Clean(filepath.Dir(file)) + for { + if manifest, ok := catalog.packageManifests[dir]; ok { + return packageManifestDeps(manifest) + } + if dir == "." || dir == string(filepath.Separator) { + break + } + next := filepath.Dir(dir) + if next == dir { + break + } + dir = next + } + return nil +} + +func (catalog scriptImportCatalog) hasInstalledPackage(root, file, packageName string) bool { + dir := filepath.Join(root, filepath.Dir(file)) + root = filepath.Clean(root) + for { + if packageExists(filepath.Join(dir, "node_modules", filepath.FromSlash(packageName))) || + pnpmPackageExists(filepath.Join(dir, "node_modules", ".pnpm"), packageName) { + return true + } + if filepath.Clean(dir) == root { + return false + } + next := filepath.Dir(dir) + if next == dir { + return false + } + dir = next + } +} + +func packageExists(path string) bool { + info, err := os.Stat(path) //nolint:gosec // path is constructed under scan target + return err == nil && info.IsDir() +} + +func pnpmPackageExists(store, packageName string) bool { + entries, err := os.ReadDir(store) //nolint:gosec // path is constructed under scan target + if err != nil { + return false + } + want := filepath.FromSlash(filepath.Join("node_modules", packageName)) + for _, entry := range entries { + if packageExists(filepath.Join(store, entry.Name(), want)) { + return true + } + } + return false +} + +func (catalog scriptImportCatalog) matchesTSConfigPath(root, file, specifier string) bool { + fileDir := filepath.Clean(filepath.Dir(file)) + for _, config := range catalog.tsconfigPathMaps { + if !pathContains(config.dir, fileDir) { + continue + } + for pattern, targets := range config.paths { + wildcard, ok := scriptPathPatternMatch(pattern, specifier) + if !ok { + continue + } + for _, target := range targets { + candidate := strings.ReplaceAll(target, "*", wildcard) + if resolveRelativeScriptImport(root, filepath.Join(config.dir, config.baseURL), candidate) { + return true + } + } + } + } + return false +} + +func pathContains(parent, child string) bool { + parent, child = filepath.Clean(parent), filepath.Clean(child) + return parent == "." || child == parent || strings.HasPrefix(child, parent+string(filepath.Separator)) +} + +func scriptPathPatternMatch(pattern, specifier string) (string, bool) { + if !strings.Contains(pattern, "*") { + return "", pattern == specifier + } + parts := strings.SplitN(pattern, "*", 2) + if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) { + return "", false + } + return strings.TrimSuffix(strings.TrimPrefix(specifier, parts[0]), parts[1]), true +} + +// stripScriptJSONC preserves string contents while removing the comments and +// trailing commas commonly accepted by tsconfig.json. +func stripScriptJSONC(data []byte) []byte { + source := string(data) + var b strings.Builder + inString, escaped := false, false + for i := 0; i < len(source); i++ { + ch := source[i] + if inString { + b.WriteByte(ch) + if escaped { + escaped = false + } else if ch == '\\' { + escaped = true + } else if ch == '"' { + inString = false + } + continue + } + if ch == '"' { + inString = true + b.WriteByte(ch) + continue + } + if ch == '/' && i+1 < len(source) && source[i+1] == '/' { + for i+1 < len(source) && source[i+1] != '\n' { + i++ + } + continue + } + if ch == '/' && i+1 < len(source) && source[i+1] == '*' { + i += 2 + for i < len(source) && (i+1 >= len(source) || source[i] != '*' || source[i+1] != '/') { + if source[i] == '\n' { + b.WriteByte('\n') + } + i++ + } + i++ + continue + } + b.WriteByte(ch) + } + return []byte(strings.ReplaceAll(strings.ReplaceAll(b.String(), ",\n}", "\n}"), ",\n]", "\n]")) +} diff --git a/internal/codeguard/checks/quality/quality_ai_target_script.go b/internal/codeguard/checks/quality/quality_ai_target_script.go index 550f5dd..7c3fd76 100644 --- a/internal/codeguard/checks/quality/quality_ai_target_script.go +++ b/internal/codeguard/checks/quality/quality_ai_target_script.go @@ -20,6 +20,9 @@ type scriptImportCatalog struct { hasManifest bool deps map[string]struct{} workspacePackage map[string]struct{} + packageManifests map[string]packageManifest + tsconfigPathMaps []scriptTSConfigPathMap + lockPackages map[string]struct{} } func typeScriptAITargetFindings(env support.Context, target core.TargetConfig) []core.Finding { @@ -27,12 +30,8 @@ func typeScriptAITargetFindings(env support.Context, target core.TargetConfig) [ if len(files) == 0 { return nil } - manifest, hasManifest := readPackageManifest(target.Path) - catalog := scriptImportCatalog{ - hasManifest: hasManifest, - deps: packageManifestDeps(manifest), - workspacePackage: readWorkspacePackageNames(env, target), - } + manifest, _ := readPackageManifest(target.Path) + catalog := newScriptImportCatalog(env, target, manifest) dominant := dominantScriptTestFramework(env, target, files, manifest) input := scriptFileScanInput{ catalog: catalog, @@ -114,23 +113,44 @@ func scriptImportResolvable(root string, file string, specifier string, catalog if _, ok := catalog.workspacePackage[rootPackage]; ok { return true } + if _, ok := catalog.declaredDependenciesFor(file)[rootPackage]; ok { + return true + } if _, ok := catalog.deps[rootPackage]; ok { return true } + if catalog.hasInstalledPackage(root, file, rootPackage) { + return true + } + if _, ok := catalog.lockPackages[rootPackage]; ok { + return true + } + if catalog.matchesTSConfigPath(root, file, specifier) { + return true + } return !catalog.hasManifest } func resolveRelativeScriptImport(root string, dir string, specifier string) bool { base := filepath.Join(root, dir, filepath.FromSlash(specifier)) - candidates := []string{ - base, base + ".ts", base + ".tsx", base + ".js", base + ".jsx", - base + ".mts", base + ".cts", base + ".mjs", base + ".cjs", - filepath.Join(base, "index.ts"), filepath.Join(base, "index.tsx"), - filepath.Join(base, "index.js"), filepath.Join(base, "index.jsx"), - } - for _, candidate := range candidates { - if info, err := os.Stat(candidate); err == nil && !info.IsDir() { //nolint:gosec // stat-only existence check; candidate is joined under the scan root - return true + // TypeScript's NodeNext/Bundler resolution accepts runtime .js specifiers + // for TypeScript source (for example, ./config.js resolving to config.ts). + // Try the extensionless source path as well as the literal path. + bases := []string{base} + if ext := filepath.Ext(base); ext == ".js" || ext == ".mjs" || ext == ".cjs" { + bases = append(bases, strings.TrimSuffix(base, ext)) + } + for _, base := range bases { + candidates := []string{ + base, base + ".ts", base + ".tsx", base + ".js", base + ".jsx", + base + ".mts", base + ".cts", base + ".mjs", base + ".cjs", + filepath.Join(base, "index.ts"), filepath.Join(base, "index.tsx"), + filepath.Join(base, "index.js"), filepath.Join(base, "index.jsx"), + } + for _, candidate := range candidates { + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { //nolint:gosec // stat-only existence check; candidate is joined under the scan root + return true + } } } return false diff --git a/tests/checks/quality_ai_additional_test.go b/tests/checks/quality_ai_additional_test.go index f6a8b9b..51e054f 100644 --- a/tests/checks/quality_ai_additional_test.go +++ b/tests/checks/quality_ai_additional_test.go @@ -45,6 +45,45 @@ export const value = missing; assertFindingRulePresent(t, report, "Code Quality", "quality.ai.hallucinated-import") } +func TestQualityCheckResolvesTypeScriptPNPMMonorepoImports(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "package.json"), `{"name":"repo","private":true}`) + writeFile(t, filepath.Join(dir, "pnpm-workspace.yaml"), "packages:\n - packages/*\n") + writeFile(t, filepath.Join(dir, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n\npackages:\n\n lock-only-package@1.0.0:\n resolution: {integrity: sha512-example}\n") + writeFile(t, filepath.Join(dir, "packages", "app", "package.json"), `{ + "name":"@legal-nest/app", + "dependencies":{"react":"18.0.0","next":"15.0.0","@prisma/client":"6.0.0"} +}`) + writeFile(t, filepath.Join(dir, "packages", "shared", "package.json"), `{"name":"@legal-nest/shared"}`) + writeFile(t, filepath.Join(dir, "packages", "app", "tsconfig.json"), `{ + // aliases are valid JSONC in tsconfig files + "compilerOptions": {"baseUrl":".", "paths":{"app/*":["src/*"]}} +}`) + writeFile(t, filepath.Join(dir, "packages", "app", "src", "config.ts"), "export const config = {};\n") + writeFile(t, filepath.Join(dir, "packages", "app", "src", "lib", "value.ts"), "export const value = 1;\n") + writeFile(t, filepath.Join(dir, "node_modules", ".pnpm", "installed-package@1.0.0", "node_modules", "installed-package", "package.json"), `{"name":"installed-package"}`) + writeFile(t, filepath.Join(dir, "packages", "app", "src", "app.ts"), ` +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { prisma } from "@prisma/client"; +import { config } from "./config.js"; +import { value } from "app/lib/value"; +import { shared } from "@legal-nest/shared"; +import installed from "installed-package"; +import lockOnly from "lock-only-package"; +void useState; void useRouter; void prisma; void config; void value; void shared; void installed; void lockOnly; +`) + + cfg := qualityAITestConfig(dir, "quality-ai-ts-pnpm-monorepo") + cfg.Targets[0].Language = "typescript" + report, err := codeguard.Run(context.Background(), cfg) + if err != nil { + t.Fatalf("run: %v", err) + } + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.ai.hallucinated-import") +} + func TestQualityCheckWarnsForDeadCode(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "dead.go"), `package sample From fcec2dc2d14420b9ed23df1ab5cd297e3865ce21 Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Fri, 24 Jul 2026 18:21:42 -0400 Subject: [PATCH 2/4] fix: satisfy import resolver lint --- .../checks/quality/quality_ai_resolution.go | 22 ------------------- .../quality/quality_ai_script_resolution.go | 7 +++--- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/internal/codeguard/checks/quality/quality_ai_resolution.go b/internal/codeguard/checks/quality/quality_ai_resolution.go index 40d5ecd..e016ea0 100644 --- a/internal/codeguard/checks/quality/quality_ai_resolution.go +++ b/internal/codeguard/checks/quality/quality_ai_resolution.go @@ -9,9 +9,6 @@ import ( "slices" "strings" "time" - - "github.com/devr-tools/codeguard/internal/codeguard/checks/support" - "github.com/devr-tools/codeguard/internal/codeguard/core" ) type packageManifest struct { @@ -57,25 +54,6 @@ func packageManifestDeps(manifest packageManifest) map[string]struct{} { return deps } -func readWorkspacePackageNames(env support.Context, target core.TargetConfig) map[string]struct{} { - files := listAITargetFiles(env, target, func(rel string) bool { - return filepath.Base(rel) == "package.json" - }) - names := map[string]struct{}{} - for _, rel := range files { - data, err := readAITargetFile(env, target, rel) - if err != nil { - continue - } - manifest, ok := parsePackageManifest(data) - if !ok || strings.TrimSpace(manifest.Name) == "" { - continue - } - names[strings.TrimSpace(manifest.Name)] = struct{}{} - } - return names -} - func readGitHeadMessage(dir string) string { // TODO(harden): thread caller ctx once readGitHeadMessage accepts one. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) diff --git a/internal/codeguard/checks/quality/quality_ai_script_resolution.go b/internal/codeguard/checks/quality/quality_ai_script_resolution.go index cab96f6..455897d 100644 --- a/internal/codeguard/checks/quality/quality_ai_script_resolution.go +++ b/internal/codeguard/checks/quality/quality_ai_script_resolution.go @@ -230,11 +230,12 @@ func stripScriptJSONC(data []byte) []byte { ch := source[i] if inString { b.WriteByte(ch) - if escaped { + switch { + case escaped: escaped = false - } else if ch == '\\' { + case ch == '\\': escaped = true - } else if ch == '"' { + case ch == '"': inString = false } continue From dc61f7e840b5c5b010de864e4cc04c789f6613cf Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Fri, 24 Jul 2026 18:22:27 -0400 Subject: [PATCH 3/4] fix: reduce AI quality false positives --- .../codeguard/checks/quality/quality_ai.go | 12 ++++ .../quality/quality_ai_dead_code_script.go | 22 ++++++- .../checks/quality/quality_ai_helpers.go | 11 +++- tests/checks/quality_ai_dead_code_test.go | 63 +++++++++++++++++++ tests/checks/quality_ai_test.go | 20 ++++++ 5 files changed, 124 insertions(+), 4 deletions(-) diff --git a/internal/codeguard/checks/quality/quality_ai.go b/internal/codeguard/checks/quality/quality_ai.go index 57fbace..11a1940 100644 --- a/internal/codeguard/checks/quality/quality_ai.go +++ b/internal/codeguard/checks/quality/quality_ai.go @@ -68,7 +68,19 @@ func typeScriptAIQualityFindings(ctx typeScriptScanContext) []core.Finding { findings = append(findings, warnFinding(ctx.env, "quality.ai.swallowed-error", ctx.file, line, 1, support.ScriptLabelForPath(ctx.file)+" catch block swallows the error without handling it")) } + inJSDoc := false for idx, line := range strings.Split(ctx.source, "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "/**") { + inJSDoc = !strings.Contains(trimmed, "*/") + continue + } + if inJSDoc { + if strings.Contains(trimmed, "*/") { + inJSDoc = false + } + continue + } text, ok := extractScriptCommentText(line) if !ok || !isNarrativeComment(text) { continue diff --git a/internal/codeguard/checks/quality/quality_ai_dead_code_script.go b/internal/codeguard/checks/quality/quality_ai_dead_code_script.go index e8504cb..dc43402 100644 --- a/internal/codeguard/checks/quality/quality_ai_dead_code_script.go +++ b/internal/codeguard/checks/quality/quality_ai_dead_code_script.go @@ -15,6 +15,8 @@ var ( scriptTerminatorPattern = regexp.MustCompile(`^(?:return\b[^;{}]*;|throw\b[^;{}]*;|break\s*;|continue\s*;|return;?$|break$|continue$)`) scriptBlockResumePattern = regexp.MustCompile(`^(?:\}|case\b|default\s*:|else\b|catch\b|finally\b)`) scriptLocalFunctionPattern = regexp.MustCompile(`(?m)^[ \t]*(?:async[ \t]+)?function[ \t]+([A-Za-z_$][\w$]*)[ \t]*\(`) + scriptLocalConstPattern = regexp.MustCompile(`(?m)^[ \t]*(?:const|let)[ \t]+([A-Za-z_$][\w$]*)[ \t]*(?::[^=\n]+)?=[ \t]*(?:async[ \t]+)?(?:\([^\n]*\)|[A-Za-z_$][\w$]*)[ \t]*=>`) + scriptControlHeaderPattern = regexp.MustCompile(`^(?:if|while|for|with)\b.*\)[ \t]*$`) ) // unreachableStatementFinding builds the shared dead-code finding emitted when @@ -29,6 +31,7 @@ func scriptUnreachableFindings(env support.Context, file string, source string) sanitized := sanitizeScriptSource(source) depth := 0 pendingDepth := -1 + previousWasUnbracedControlHeader := false for idx, line := range strings.Split(sanitized, "\n") { trimmed := strings.TrimSpace(line) if trimmed == "" { @@ -42,9 +45,10 @@ func scriptUnreachableFindings(env support.Context, file string, source string) } pendingDepth = -1 } - if scriptTerminatorPattern.MatchString(trimmed) && balancedParens(trimmed) { + if scriptTerminatorPattern.MatchString(trimmed) && balancedParens(trimmed) && !previousWasUnbracedControlHeader { pendingDepth = depth } + previousWasUnbracedControlHeader = scriptControlHeaderPattern.MatchString(trimmed) && !strings.Contains(trimmed, "{") } return findings } @@ -58,14 +62,14 @@ func balancedParens(line string) bool { func scriptUnusedFunctionFindings(env support.Context, file string, source string) []core.Finding { sanitized := sanitizeScriptSource(source) findings := make([]core.Finding, 0) - for _, match := range scriptLocalFunctionPattern.FindAllStringSubmatchIndex(sanitized, -1) { + for _, match := range scriptLocalDeclarationMatches(sanitized) { name := sanitized[match[2]:match[3]] lineStart := strings.LastIndexByte(sanitized[:match[0]], '\n') + 1 declLine := sanitized[lineStart:lineEnd(sanitized, match[0])] if strings.Contains(declLine, "export") { continue } - if countWordOccurrences(sanitized, name) > 1 { + if scriptLocalDeclarationIsReferenced(sanitized, name) { continue } line := 1 + strings.Count(sanitized[:match[2]], "\n") @@ -75,6 +79,18 @@ func scriptUnusedFunctionFindings(env support.Context, file string, source strin return findings } +func scriptLocalDeclarationIsReferenced(source string, name string) bool { + // JSX identifiers are references, not opaque markup. Check them directly + // before falling back to ordinary expression references. + jsxReference := regexp.MustCompile(`<\s*` + regexp.QuoteMeta(name) + `(?:\s|/|>)`) + return jsxReference.MatchString(source) || countWordOccurrences(source, name) > 1 +} + +func scriptLocalDeclarationMatches(source string) [][]int { + matches := append(scriptLocalFunctionPattern.FindAllStringSubmatchIndex(source, -1), scriptLocalConstPattern.FindAllStringSubmatchIndex(source, -1)...) + return matches +} + func lineEnd(source string, from int) int { if idx := strings.IndexByte(source[from:], '\n'); idx >= 0 { return from + idx diff --git a/internal/codeguard/checks/quality/quality_ai_helpers.go b/internal/codeguard/checks/quality/quality_ai_helpers.go index 65074f6..832a3c9 100644 --- a/internal/codeguard/checks/quality/quality_ai_helpers.go +++ b/internal/codeguard/checks/quality/quality_ai_helpers.go @@ -8,6 +8,7 @@ import ( var ( aiNarrativeCommentPattern = regexp.MustCompile(`(?i)^(initialize|create|set|get|call|return|check|convert|update|build|iterate|loop|run|assign|store)\b`) aiRationalePattern = regexp.MustCompile(`(?i)\b(because|so that|why|ensure|ensures|avoid|must|needed|required|reason|safely|in order to)\b`) + aiRouteCommentPattern = regexp.MustCompile(`^(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+/`) aiEmptyCatchPattern = regexp.MustCompile(`(?s)\bcatch\s*(?:\([^)]*\))?\s*\{\s*(?:(?://[^\n]*\n)|(?:/\*.*?\*/\s*))*\}`) aiPythonPassExceptPattern = regexp.MustCompile(`(?m)^\s*except(?:\s+[^\n:]+)?\s*:\s*(?:#.*)?\n\s*(pass|\.\.\.)\b`) ) @@ -20,13 +21,21 @@ func aiCheckEnabled(flag *bool) bool { func isNarrativeComment(text string) bool { trimmed := strings.TrimSpace(text) - if trimmed == "" || aiRationalePattern.MatchString(trimmed) || !aiNarrativeCommentPattern.MatchString(trimmed) { + if trimmed == "" || aiRationalePattern.MatchString(trimmed) || isCommentInstructionOrHeader(trimmed) || !aiNarrativeCommentPattern.MatchString(trimmed) { return false } words := strings.Fields(trimmed) return len(words) >= 2 && len(words) <= 10 } +func isCommentInstructionOrHeader(text string) bool { + lower := strings.ToLower(text) + return strings.HasPrefix(lower, "run:") || + strings.HasPrefix(lower, "usage:") || + strings.HasPrefix(lower, "example:") || + aiRouteCommentPattern.MatchString(text) +} + func regexLineMatches(pattern *regexp.Regexp, source string) []int { indices := pattern.FindAllStringIndex(source, -1) lines := make([]int, 0, len(indices)) diff --git a/tests/checks/quality_ai_dead_code_test.go b/tests/checks/quality_ai_dead_code_test.go index eb279a3..8f35530 100644 --- a/tests/checks/quality_ai_dead_code_test.go +++ b/tests/checks/quality_ai_dead_code_test.go @@ -197,6 +197,69 @@ function helper(): number { assertFindingRuleAbsent(t, report, "Code Quality", "quality.ai.dead-code") } +func TestQualityCheckAllowsJSXAndExpressionReferencesToLocalHelpers(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "page.tsx"), `function StatusBadge({ status }: { status: string }) { + return {status}; +} + +const Row = ({ children }: { children: unknown }) =>
{children}
; + +function fmtDate(value: string): string { + return value; +} + +export function Page({ status, nextReviewDate }: { status: string; nextReviewDate: string }) { + return {fmtDate(nextReviewDate)}; +} +`) + + cfg := qualityAITestConfig(dir, "quality-ai-tsx-local-references") + cfg.Targets[0].Language = "typescript" + report, err := codeguard.Run(context.Background(), cfg) + if err != nil { + t.Fatalf("run: %v", err) + } + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.ai.dead-code") +} + +func TestQualityCheckAllowsStatementAfterUnbracedConditionalReturn(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "handler.ts"), `export function run(done: boolean): string { + if (done) + return "complete"; + return "pending"; +} +`) + + cfg := qualityAITestConfig(dir, "quality-ai-ts-unbraced-return") + cfg.Targets[0].Language = "typescript" + report, err := codeguard.Run(context.Background(), cfg) + if err != nil { + t.Fatalf("run: %v", err) + } + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.ai.dead-code") +} + +func TestQualityCheckWarnsForUnusedLocalTypeScriptArrowFunction(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "handler.ts"), `export const run = () => "ok"; + +const orphanHelper = () => "unused"; +`) + + cfg := qualityAITestConfig(dir, "quality-ai-ts-unused-arrow") + cfg.Targets[0].Language = "typescript" + report, err := codeguard.Run(context.Background(), cfg) + if err != nil { + t.Fatalf("run: %v", err) + } + + assertFindingRulePresent(t, report, "Code Quality", "quality.ai.dead-code") +} + func TestQualityCheckHonorsDeadCodeToggle(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "unreachable.go"), `package sample diff --git a/tests/checks/quality_ai_test.go b/tests/checks/quality_ai_test.go index 145dece..f874b8e 100644 --- a/tests/checks/quality_ai_test.go +++ b/tests/checks/quality_ai_test.go @@ -48,6 +48,26 @@ func buildClient() {} assertFindingConfidence(t, report, "Code Quality", "quality.ai.narrative-comment", "low") } +func TestQualityCheckAllowsUsefulTypeScriptComments(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "route.ts"), `/** GET /api/files/[versionId]/download — stream a single FileVersion. */ +// Run: pnpm --filter @legal-nest/db exec tsx prisma/seed.ts +// Autoscroll on new turn in a useEffect +// Dispatch on entity type. Prisma accepts string indexing, but TS does not model it. +// Higher-is-worse fields default to desc; priorities and labels default to asc. +export function route() {} +`) + + cfg := qualityAITestConfig(dir, "quality-ai-useful-comments") + cfg.Targets[0].Language = "typescript" + report, err := codeguard.Run(context.Background(), cfg) + if err != nil { + t.Fatalf("run: %v", err) + } + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.ai.narrative-comment") +} + func TestQualityCheckWarnsForEmptyCatchInTypeScript(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "handler.ts"), `export function run() { From 11acbd635c1446b8a7237a6cd3403c758c0deb49 Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Fri, 24 Jul 2026 20:01:05 -0400 Subject: [PATCH 4/4] fix: parse pnpm lockfile package entries --- .../codeguard/checks/quality/quality_ai_script_resolution.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/codeguard/checks/quality/quality_ai_script_resolution.go b/internal/codeguard/checks/quality/quality_ai_script_resolution.go index 455897d..f9c230a 100644 --- a/internal/codeguard/checks/quality/quality_ai_script_resolution.go +++ b/internal/codeguard/checks/quality/quality_ai_script_resolution.go @@ -85,6 +85,9 @@ func readPNPMLockPackages(root string) map[string]struct{} { packages := map[string]struct{}{} inPackages := false for _, line := range strings.Split(string(data), "\n") { + if strings.TrimSpace(line) == "" { + continue + } if !strings.HasPrefix(line, " ") { inPackages = strings.TrimSpace(line) == "packages:" continue