From c4f61c961f633449daf1ebb251c54058508b1d4b Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Mon, 27 Jul 2026 19:27:22 -0400 Subject: [PATCH 1/4] fix: reduce hidden mutation noise for React hooks --- .../quality/quality_precision_react_hooks.go | 93 +++++++++++++++++++ .../quality_precision_workstreams_cd.go | 13 ++- tests/checks/function_precision_test.go | 75 +++++++++++++++ 3 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 internal/codeguard/checks/quality/quality_precision_react_hooks.go diff --git a/internal/codeguard/checks/quality/quality_precision_react_hooks.go b/internal/codeguard/checks/quality/quality_precision_react_hooks.go new file mode 100644 index 0000000..fa8c764 --- /dev/null +++ b/internal/codeguard/checks/quality/quality_precision_react_hooks.go @@ -0,0 +1,93 @@ +package quality + +import ( + "regexp" + "strings" + "unicode" +) + +var reactHookNamePattern = regexp.MustCompile(`^use[A-Z0-9]`) + +func onlyReactHookLocalStateMutation(fn precisionFunction) bool { + sawLocalStateMutation := false + for _, assignment := range fn.Assignments { + if assignment.Augmented { + return false + } + } + for _, call := range fn.Calls { + if !mutatingCallPattern.MatchString(call.Callee) { + continue + } + if !isReactLocalStateMutationCall(call.Callee) { + return false + } + sawLocalStateMutation = true + } + return sawLocalStateMutation +} + +func isReactHookStateBoundary(file string, fn precisionFunction) bool { + if !isScriptLikeSourcePath(file) { + return false + } + if isReactHookName(fn.Name) { + return functionUsesReactState(fn) || callsReactLocalStateSetter(fn) + } + if isReactHookFile(file) { + return callsReactLocalStateSetter(fn) + } + return false +} + +func isScriptLikeSourcePath(file string) bool { + lowered := strings.ToLower(file) + return strings.HasSuffix(lowered, ".ts") || strings.HasSuffix(lowered, ".tsx") || + strings.HasSuffix(lowered, ".js") || strings.HasSuffix(lowered, ".jsx") +} + +func isReactHookFile(file string) bool { + normalized := strings.ReplaceAll(file, "\\", "/") + base := normalized + if slash := strings.LastIndex(base, "/"); slash >= 0 { + base = base[slash+1:] + } + lowered := strings.ToLower(base) + return strings.HasPrefix(lowered, "use-") || reactHookNamePattern.MatchString(base) +} + +func isReactHookName(name string) bool { + return reactHookNamePattern.MatchString(strings.TrimSpace(name)) +} + +func functionUsesReactState(fn precisionFunction) bool { + for _, call := range fn.Calls { + if call.Callee == "useState" || call.Callee == "React.useState" || call.Callee == "useReducer" || call.Callee == "React.useReducer" { + return true + } + } + return strings.Contains(fn.Body, "useState(") || strings.Contains(fn.Body, "useReducer(") +} + +func callsReactLocalStateSetter(fn precisionFunction) bool { + for _, call := range fn.Calls { + if isReactLocalStateMutationCall(call.Callee) { + return true + } + } + return false +} + +func isReactLocalStateMutationCall(callee string) bool { + if strings.ContainsAny(callee, ".:>") { + return false + } + if callee == "dispatch" { + return true + } + if len(callee) <= len("set") || !strings.HasPrefix(callee, "set") { + return false + } + next := rune(callee[len("set")]) + return unicode.IsUpper(next) || unicode.IsDigit(next) +} diff --git a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go index 3ff46d6..9abfb60 100644 --- a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go +++ b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go @@ -30,7 +30,7 @@ const ( ) var ( - commandFunctionPrefixPattern = regexp.MustCompile(`^(add|append|assign|cancel|create|delete|emit|insert|mutate|persist|publish|remove|save|send|set|store|update|upsert|write)`) + commandFunctionPrefixPattern = regexp.MustCompile(`^(add|append|assign|cancel|clear|close|create|delete|disable|emit|enable|insert|mutate|open|persist|publish|remove|reset|save|send|set|store|toggle|update|upsert|write)`) readCallPattern = regexp.MustCompile(`(?i)(^|[.>:\-_])(count|fetch|find|get|list|load|lookup|query|read|select|search)([A-Z_:\-.]|$)`) identifierTokenPattern = regexp.MustCompile(`[A-Za-z_$][A-Za-z0-9_$]*`) infraNamePattern = regexp.MustCompile(`(?i)(sql|http|redis|kafka|grpc|graphql|mongo|s3|dynamo|postgres|mysql|elastic|orm)`) @@ -54,7 +54,7 @@ func additionalPrecisionFunctionFindings(env support.Context, file string, fn pr findings = append(findings, precisionWarnFinding(env, namingBehaviorMismatchRuleID, file, fn.StartLine, fmt.Sprintf("function %s name conflicts with observed query/command behavior", fn.Name), core.ConfidenceMedium)) } - if hiddenMutation(fn) { + if hiddenMutation(file, fn) { findings = append(findings, precisionWarnFinding(env, functionHiddenMutationRuleID, file, fn.StartLine, fmt.Sprintf("function %s mutates state without an explicit command-style name", fn.Name), core.ConfidenceMedium)) } @@ -169,11 +169,16 @@ func behaviorMismatch(fn precisionFunction) bool { return false } -func hiddenMutation(fn precisionFunction) bool { +func hiddenMutation(file string, fn precisionFunction) bool { if explicitMutationName(fn.Name) { return false } - return mutatingFunctionEvidence(fn) || mutatesParameter(fn) + mutatesParam := mutatesParameter(fn) + mutatesState := mutatingFunctionEvidence(fn) + if isReactHookStateBoundary(file, fn) && mutatesState && !mutatesParam && onlyReactHookLocalStateMutation(fn) { + return false + } + return mutatesState || mutatesParam } func mutatingFunctionEvidence(fn precisionFunction) bool { diff --git a/tests/checks/function_precision_test.go b/tests/checks/function_precision_test.go index 556c8f0..7dc2711 100644 --- a/tests/checks/function_precision_test.go +++ b/tests/checks/function_precision_test.go @@ -155,6 +155,81 @@ func TestFunctionHiddenMutationWarnsAcrossLanguages(t *testing.T) { } } +func TestFunctionHiddenMutationAllowsReactHookLocalStateBoundaries(t *testing.T) { + cases := []struct { + name string + language string + file string + source []string + }{ + { + name: "typescript", + language: "typescript", + file: "apps/web/src/hooks/use-filters.ts", + source: []string{ + "import { useCallback, useReducer, useState } from 'react';", + "export function useFilters() {", + " const [filters, setFilters] = useState({});", + " const [state, dispatch] = useReducer(reducer, {});", + " function clearFilters() {", + " setFilters({});", + " }", + " const resetFilters = useCallback(() => {", + " setFilters({});", + " dispatch({ type: 'reset' });", + " }, []);", + " return { clearFilters, resetFilters, state };", + "}", + "function reducer(state: unknown, event: { type: string }) { return state; }", + }, + }, + { + name: "javascript", + language: "javascript", + file: "apps/web/src/hooks/use-filters.js", + source: []string{ + "import { useCallback, useState } from 'react';", + "export function useFilters() {", + " const [filters, setFilters] = useState({});", + " const resetFilters = useCallback(() => {", + " setFilters({});", + " }, []);", + " return { resetFilters, filters };", + "}", + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, tc.file), strings.Join(tc.source, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, tc.language)) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.hidden-mutation") + }) + } +} + +func TestFunctionHiddenMutationStillWarnsForHiddenPersistenceInsideReactHook(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/src/hooks/use-user.ts"), strings.Join([]string{ + "export function useUser(repo: Repository, user: User) {", + " function loadUser() {", + " repo.save(user);", + " return repo.find(user.id);", + " }", + " return { loadUser };", + "}", + "interface Repository { save(user: User): void; find(id: string): User }", + "interface User { id: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRulePresent(t, report, "Code Quality", "function.hidden-mutation") +} + func TestFunctionResponsibilityAndOrchestrationRules(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "handler.ts"), strings.Join([]string{ From 8f77a747d186e43cecfaecd7e9a29f84a57d5978 Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Mon, 27 Jul 2026 19:45:42 -0400 Subject: [PATCH 2/4] fix: harden hidden mutation precision --- .../quality_precision_mutation_targets.go | 110 +++++++++ .../quality_precision_workstreams_cd.go | 8 +- .../function_hidden_mutation_noise_test.go | 216 ++++++++++++++++++ 3 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 internal/codeguard/checks/quality/quality_precision_mutation_targets.go create mode 100644 tests/checks/function_hidden_mutation_noise_test.go diff --git a/internal/codeguard/checks/quality/quality_precision_mutation_targets.go b/internal/codeguard/checks/quality/quality_precision_mutation_targets.go new file mode 100644 index 0000000..188dbf0 --- /dev/null +++ b/internal/codeguard/checks/quality/quality_precision_mutation_targets.go @@ -0,0 +1,110 @@ +package quality + +import ( + "path/filepath" + "regexp" + "strings" +) + +var conventionalMutationBoundaryPattern = regexp.MustCompile(`^(accept|apply|approve|archive|clear|close|commit|deliver|download|drop|ensure|exists|fetch|import|list|notify|open|process|read|reconcile|record|run|seed|submit|sync|toggle|upload)`) + +func localMutationTargets(fn precisionFunction) map[string]struct{} { + params := paramNames(fn) + targets := make(map[string]struct{}) + for _, assignment := range fn.Assignments { + name := strings.TrimSpace(assignment.Name) + if name == "" || assignment.Augmented { + continue + } + if _, isParam := params[name]; isParam { + continue + } + targets[name] = struct{}{} + } + return targets +} + +func paramNames(fn precisionFunction) map[string]struct{} { + params := make(map[string]struct{}, len(fn.Params)) + for _, param := range fn.Params { + if param.Name != "" { + params[param.Name] = struct{}{} + } + } + return params +} + +func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool { + if isBareLocalMutationCall(callee) { + return true + } + target := mutationCallTarget(callee) + if target == "" { + return false + } + return isLocalMutationTarget(target, localTargets) +} + +func isBareLocalMutationCall(callee string) bool { + switch strings.TrimSpace(callee) { + case "append", "Set": + return true + default: + return false + } +} + +func mutationCallTarget(callee string) string { + callee = strings.TrimSpace(callee) + if callee == "" { + return "" + } + for _, sep := range []string{".", "->", "::"} { + if idx := strings.Index(callee, sep); idx > 0 { + return strings.TrimSpace(callee[:idx]) + } + } + return "" +} + +func isLocalMutationTarget(name string, localTargets map[string]struct{}) bool { + _, ok := localTargets[strings.TrimSpace(name)] + return ok +} + +func isFrameworkCommandBoundary(file string, name string) bool { + if !isScriptLikeSourcePath(file) { + return false + } + if !isHTTPMethodName(name) { + return false + } + normalized := strings.ReplaceAll(file, "\\", "/") + return strings.HasSuffix(normalized, "/route.ts") || strings.HasSuffix(normalized, "/route.tsx") || + strings.HasSuffix(normalized, "/route.js") || strings.HasSuffix(normalized, "/route.jsx") +} + +func isHTTPMethodName(name string) bool { + switch strings.ToUpper(strings.TrimSpace(name)) { + case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS": + return true + default: + return false + } +} + +func isScriptEntrypoint(file string, name string) bool { + if strings.TrimSpace(name) != "main" { + return false + } + normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) + base := filepath.Base(normalized) + if strings.HasPrefix(base, "seed") || strings.HasPrefix(base, "backfill") || strings.HasPrefix(base, "import") || strings.HasPrefix(base, "cleanup") { + return true + } + return strings.Contains(normalized, "/scripts/") || + strings.Contains(normalized, "/script/") || + strings.Contains(normalized, "/backfill") || + strings.Contains(normalized, "/seed") || + strings.Contains(normalized, "/import") +} diff --git a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go index 9abfb60..97f4cc7 100644 --- a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go +++ b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go @@ -170,7 +170,7 @@ func behaviorMismatch(fn precisionFunction) bool { } func hiddenMutation(file string, fn precisionFunction) bool { - if explicitMutationName(fn.Name) { + if explicitMutationName(fn.Name) || isFrameworkCommandBoundary(file, fn.Name) || isScriptEntrypoint(file, fn.Name) { return false } mutatesParam := mutatesParameter(fn) @@ -182,13 +182,14 @@ func hiddenMutation(file string, fn precisionFunction) bool { } func mutatingFunctionEvidence(fn precisionFunction) bool { + localTargets := localMutationTargets(fn) for _, call := range fn.Calls { - if mutatingCallPattern.MatchString(call.Callee) { + if mutatingCallPattern.MatchString(call.Callee) && !isLocalMutationCall(call.Callee, localTargets) { return true } } for _, assignment := range fn.Assignments { - if assignment.Augmented { + if assignment.Augmented && !isLocalMutationTarget(assignment.Name, localTargets) { return true } } @@ -242,6 +243,7 @@ func lineHasAssignmentOperator(line string) bool { func explicitMutationName(name string) bool { lowered := strings.ToLower(strings.TrimSpace(name)) return commandFunctionPrefixPattern.MatchString(lowered) || + conventionalMutationBoundaryPattern.MatchString(lowered) || strings.Contains(lowered, "mutat") || strings.Contains(lowered, "persist") || strings.Contains(lowered, "write") diff --git a/tests/checks/function_hidden_mutation_noise_test.go b/tests/checks/function_hidden_mutation_noise_test.go new file mode 100644 index 0000000..d45410a --- /dev/null +++ b/tests/checks/function_hidden_mutation_noise_test.go @@ -0,0 +1,216 @@ +package checks_test + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestFunctionHiddenMutationAllowsPureLocalMutationAcrossLanguages(t *testing.T) { + cases := []struct { + name string + language string + file string + source []string + }{ + { + name: "typescript local map", + language: "typescript", + file: "packages/api/src/lib/contract-summary/parse.ts", + source: []string{ + "export function parseSummary(rows: Row[]): Map {", + " const totals = new Map();", + " for (const row of rows) {", + " totals.set(row.key, row.total);", + " }", + " return totals;", + "}", + "interface Row { key: string; total: number }", + }, + }, + { + name: "javascript local set", + language: "javascript", + file: "apps/web/lib/sla.js", + source: []string{ + "export function buildScopes(events) {", + " const scopes = new Set();", + " for (const event of events) {", + " scopes.add(event.scope);", + " }", + " return Array.from(scopes);", + "}", + }, + }, + { + name: "python local list", + language: "python", + file: "packages/auth/src/resolve.py", + source: []string{ + "def data_derived_filters(rows):", + " filters = []", + " for row in rows:", + " filters.append(row.filter)", + " return filters", + }, + }, + { + name: "go local builder", + language: "go", + file: "report.go", + source: []string{ + "package sample", + "import \"strings\"", + "func RenderReport(rows []string) string {", + "\tvar builder strings.Builder", + "\tfor _, row := range rows {", + "\t\tbuilder.WriteString(row)", + "\t}", + "\treturn builder.String()", + "}", + }, + }, + { + name: "cpp local collection", + language: "cpp", + file: "filters.cpp", + source: []string{ + "#include ", + "std::set BuildFilters() {", + " std::set filters = {};", + " filters.insert(1);", + " return filters;", + "}", + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, tc.file), strings.Join(tc.source, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, tc.language)) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.hidden-mutation") + }) + } +} + +func TestFunctionHiddenMutationStillWarnsForCollaboratorMutationWithLocalPayload(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "mutation.ts"), strings.Join([]string{ + "export function prepareDigest(repo: Repository, message: Message) {", + " const payload = new Map();", + " payload.set('subject', message.subject);", + " repo.save(payload);", + " return payload;", + "}", + "interface Repository { save(input: unknown): void }", + "interface Message { subject: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRulePresent(t, report, "Code Quality", "function.hidden-mutation") +} + +func TestFunctionHiddenMutationAllowsNextRouteHandlerNames(t *testing.T) { + cases := []struct { + name string + file string + source []string + }{ + { + name: "get", + file: "apps/web/app/api/files/[versionId]/download/route.ts", + source: []string{ + "export async function GET(request: Request) {", + " await audit.save(request);", + " return Response.json({ ok: true });", + "}", + }, + }, + { + name: "post", + file: "apps/web/app/api/files/upload/route.ts", + source: []string{ + "export async function POST(request: Request) {", + " await storage.upload(request);", + " await audit.record(request);", + " return Response.json({ ok: true });", + "}", + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, tc.file), strings.Join(tc.source, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.hidden-mutation") + }) + } +} + +func TestFunctionHiddenMutationAllowsConventionalCommandNames(t *testing.T) { + cases := []struct { + name string + }{ + {name: "submit"}, + {name: "uploadOne"}, + {name: "downloadCsv"}, + {name: "notify"}, + {name: "fetchDigestMessages"}, + {name: "listWorkspaceUsers"}, + {name: "read"}, + {name: "exists"}, + {name: "seedContracts"}, + {name: "applyDiscrepancyMerge"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "commands.ts"), strings.Join([]string{ + "export async function " + tc.name + "(repo: Repository, input: Input) {", + " await repo.save(input);", + " return repo.find(input.id);", + "}", + "interface Repository { save(input: Input): Promise; find(id: string): Promise }", + "interface Input { id: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.hidden-mutation") + }) + } +} + +func TestFunctionHiddenMutationAllowsScriptMainEntrypoints(t *testing.T) { + cases := []struct { + name string + file string + }{ + {name: "script directory", file: "apps/web/scripts/backfill-slack-profiles.ts"}, + {name: "cleanup file", file: "packages/db/prisma/cleanup-phantom-users.ts"}, + {name: "import file", file: "packages/db/prisma/import-drive-policies.ts"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, tc.file), strings.Join([]string{ + "async function main() {", + " await repo.save({ ok: true });", + " await notifier.send({ ok: true });", + "}", + "main();", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.hidden-mutation") + }) + } +} From b46dfe77d5fe308e45e8734375b1a6e0bf983bb2 Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Mon, 27 Jul 2026 19:57:56 -0400 Subject: [PATCH 3/4] fix: harden UI false positive precision --- .../checks/design/local_abstraction.go | 51 +++++- .../checks/quality/quality_precision.go | 58 +++++- .../quality_precision_mutation_targets.go | 36 +++- .../quality_precision_ui_conventions.go | 141 +++++++++++++++ .../quality_precision_workstreams_cd.go | 29 +-- tests/checks/design_local_abstraction_test.go | 31 ++++ .../function_hidden_mutation_noise_test.go | 18 ++ ...uality_ui_false_positive_hardening_test.go | 168 ++++++++++++++++++ 8 files changed, 498 insertions(+), 34 deletions(-) create mode 100644 internal/codeguard/checks/quality/quality_precision_ui_conventions.go create mode 100644 tests/checks/quality_ui_false_positive_hardening_test.go diff --git a/internal/codeguard/checks/design/local_abstraction.go b/internal/codeguard/checks/design/local_abstraction.go index 2e29b69..c9808fd 100644 --- a/internal/codeguard/checks/design/local_abstraction.go +++ b/internal/codeguard/checks/design/local_abstraction.go @@ -121,7 +121,8 @@ func leakFindings(env support.Context, file string, source string) []core.Findin findings = append(findings, designFinding(env, ruleInfrastructureLeak, file, lineNo, "infrastructure/framework type leaks into a domain or public boundary", core.ConfidenceHigh)) } - if (apiPath || handlerPath || isPublicDeclaration(trimmed)) && persistenceLeakPattern.MatchString(trimmed) { + if (apiPath || handlerPath || isPublicDeclaration(trimmed)) && persistenceLeakPattern.MatchString(trimmed) && + !allowedGeneratedPersistenceEnumLine(trimmed) && !allowedTypeScriptRecordUtilityLine(trimmed) { findings = append(findings, designFinding(env, rulePersistenceLeak, file, lineNo, "persistence model or ORM concept leaks through a public/API boundary", core.ConfidenceHigh)) } @@ -136,6 +137,54 @@ func leakFindings(env support.Context, file string, source string) []core.Findin return firstFindingPerRule(findings) } +func allowedTypeScriptRecordUtilityLine(line string) bool { + return strings.Contains(line, "Record<") && + !strings.Contains(line, "PrismaClient") && + !strings.Contains(line, "Model") && + !strings.Contains(line, "Entity") && + !strings.Contains(line, "Row") +} + +func allowedGeneratedPersistenceEnumLine(line string) bool { + lowered := strings.ToLower(line) + if !strings.Contains(lowered, "from") || !strings.Contains(lowered, "@prisma/client") { + return false + } + if strings.Contains(line, "PrismaClient") || strings.Contains(line, "Prisma.") { + return false + } + open := strings.Index(line, "{") + close := strings.Index(line, "}") + if open < 0 || close <= open { + return false + } + for _, part := range strings.Split(line[open+1:close], ",") { + name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(part), "type ")) + if name == "" { + continue + } + if strings.Contains(strings.ToLower(name), " as ") { + name = strings.TrimSpace(strings.SplitN(name, " as ", 2)[0]) + } + if !looksLikeGeneratedEnumType(name) { + return false + } + } + return true +} + +func looksLikeGeneratedEnumType(name string) bool { + if strings.Contains(name, "Client") || strings.Contains(name, "Model") || strings.Contains(name, "Record") || + strings.Contains(name, "Row") || strings.Contains(name, "Entity") { + return false + } + if name == "" { + return false + } + first := rune(name[0]) + return first >= 'A' && first <= 'Z' +} + func domainLogicHandlerFinding(env support.Context, file string, lines []string) []core.Finding { score := 0 lineNo := 1 diff --git a/internal/codeguard/checks/quality/quality_precision.go b/internal/codeguard/checks/quality/quality_precision.go index cb55cec..2d7e2a6 100644 --- a/internal/codeguard/checks/quality/quality_precision.go +++ b/internal/codeguard/checks/quality/quality_precision.go @@ -383,7 +383,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun findings = append(findings, precisionWarnFinding(env, namingGenericIdentifierRuleID, file, fn.StartLine, fmt.Sprintf("function name %q is too generic to communicate intent", fn.Name), core.ConfidenceHigh)) } - if isAmbiguousIdentifier(fn.Name) { + if isAmbiguousIdentifier(fn.Name) && !isUIConventionalAmbiguousName(file, fn, fn.Name, "", fn.StartLine) { findings = append(findings, precisionWarnFinding(env, qualityAmbiguousNameRuleID, file, fn.StartLine, fmt.Sprintf("function name %q is ambiguous without domain context", fn.Name), core.ConfidenceHigh)) } @@ -392,7 +392,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun findings = append(findings, precisionWarnFinding(env, namingGenericIdentifierRuleID, file, fn.StartLine, fmt.Sprintf("parameter %q is too generic to communicate intent", param.Name), core.ConfidenceHigh)) } - if isAmbiguousIdentifier(param.Name) { + if isAmbiguousIdentifier(param.Name) && !isUIConventionalAmbiguousName(file, fn, param.Name, param.Type, fn.StartLine) { findings = append(findings, precisionWarnFinding(env, qualityAmbiguousNameRuleID, file, fn.StartLine, fmt.Sprintf("parameter %q is ambiguous without domain context", param.Name), core.ConfidenceHigh)) } @@ -406,7 +406,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun findings = append(findings, precisionWarnFinding(env, namingGenericIdentifierRuleID, file, assignment.Line, fmt.Sprintf("identifier %q is too generic to explain its role", assignment.Name), core.ConfidenceHigh)) } - if isAmbiguousIdentifier(assignment.Name) { + if isAmbiguousIdentifier(assignment.Name) && !isUIConventionalAmbiguousName(file, fn, assignment.Name, "", assignment.Line) { findings = append(findings, precisionWarnFinding(env, qualityAmbiguousNameRuleID, file, assignment.Line, fmt.Sprintf("identifier %q is ambiguous without domain context", assignment.Name), core.ConfidenceHigh)) } @@ -417,7 +417,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun findings = append(findings, precisionWarnFinding(env, qualityMixedAbstractionLevelsRuleID, file, fn.StartLine, fmt.Sprintf("function %s mixes domain intent with low-level implementation details", fn.Name), core.ConfidenceMedium)) } - if commandQueryMix(fn) { + if commandQueryMix(file, fn) { findings = append(findings, precisionWarnFinding(env, functionCommandQueryMixRuleID, file, fn.StartLine, fmt.Sprintf("function %s returns a value while also invoking mutating side-effect operations", fn.Name), core.ConfidenceMedium)) } @@ -516,7 +516,10 @@ func isDomainLevelCall(callee string) bool { return strings.Contains(callee, ".") || queryFunctionPrefixPattern.MatchString(lowered) || len(callee) > 3 } -func commandQueryMix(fn precisionFunction) bool { +func commandQueryMix(file string, fn precisionFunction) bool { + if isFrameworkCommandBoundary(file, fn.Name) || isReactComponentOrHookBoundary(file, fn) { + return false + } if !fn.Returns { return false } @@ -622,6 +625,9 @@ func parsedMutableGlobalFindings(env support.Context, file string, parsed *suppo if text == "" || strings.HasPrefix(text, "const ") || strings.HasPrefix(text, "final ") { continue } + if isScriptLikeSourcePath(file) && !moduleStatementLooksTopLevel(statement) { + continue + } if mutableGlobalLinePattern.MatchString(text) { findings = append(findings, precisionWarnFinding(env, qualityMutableGlobalStateRuleID, file, statement.Line, "mutable module-level state makes behavior harder to isolate and test", core.ConfidenceHigh)) @@ -639,7 +645,7 @@ func parsedDuplicatedKnowledgeFindings(env support.Context, file string, parsed for _, literal := range domainKnowledgeLiterals(statement.Raw) { if first, exists := seen[literal]; exists { return []core.Finding{precisionWarnFinding(env, qualityDuplicatedKnowledgeRuleID, file, statement.Line, - fmt.Sprintf("business literal is duplicated near line %d; centralize shared domain knowledge", first), core.ConfidenceLow)} + fmt.Sprintf("business literal %s is duplicated near line %d; centralize shared domain knowledge", literal, first), core.ConfidenceLow)} } seen[literal] = statement.Line } @@ -672,6 +678,9 @@ func sourceMutableGlobalFindings(env support.Context, file string, source string return nil } for idx, line := range strings.Split(strings.ReplaceAll(source, "\r\n", "\n"), "\n") { + if isScriptLikeSourcePath(file) && !scriptSourceLineAtModuleScope(source, idx) { + continue + } trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "const ") || strings.HasPrefix(trimmed, "final ") { @@ -697,7 +706,7 @@ func sourceDuplicatedKnowledgeFindings(env support.Context, file string, source for _, literal := range domainKnowledgeLiterals(line) { if first, exists := seen[literal]; exists { return []core.Finding{precisionWarnFinding(env, qualityDuplicatedKnowledgeRuleID, file, idx+1, - fmt.Sprintf("business literal is duplicated near line %d; centralize shared domain knowledge", first), core.ConfidenceLow)} + fmt.Sprintf("business literal %s is duplicated near line %d; centralize shared domain knowledge", literal, first), core.ConfidenceLow)} } seen[literal] = idx + 1 } @@ -714,6 +723,9 @@ func redundantCommentVerb(comment string) string { } func domainKnowledgeLiterals(line string) []string { + if duplicatedKnowledgeLineIsDisplayOnly(line) { + return nil + } matches := regexp.MustCompile(`"([^"]{2,80})"|'([^']{2,80})'|\b\d+(?:\.\d+)?\b`).FindAllString(line, -1) out := make([]string, 0, len(matches)) for _, match := range matches { @@ -724,17 +736,49 @@ func domainKnowledgeLiterals(line string) []string { return out } +func duplicatedKnowledgeLineIsDisplayOnly(line string) bool { + lowered := strings.ToLower(line) + if strings.Contains(lowered, "classname") || strings.Contains(lowered, "clasname") || strings.Contains(lowered, "class:") { + return true + } + if strings.Contains(line, "<") && strings.Contains(line, ">") { + return true + } + if strings.Contains(lowered, "label:") || strings.Contains(lowered, "placeholder:") || strings.Contains(lowered, "title:") || + strings.Contains(lowered, "aria-label") { + return true + } + return false +} + func domainKnowledgeLiteral(value string) bool { trimmed := strings.Trim(value, `"'`) if trimmed == "" || len(trimmed) > 80 { return false } + if len(trimmed) < 4 && !strings.ContainsAny(trimmed, "0123456789") { + return false + } if _, err := strconv.Atoi(trimmed); err == nil { return true } + if likelyDisplayLabel(trimmed) { + return false + } return domainPrimitiveNamePattern.MatchString(trimmed) || strings.Contains(trimmed, "_") } +func likelyDisplayLabel(value string) bool { + if strings.Contains(value, "_") { + return false + } + if strings.ContainsAny(value, "-/:.") { + return false + } + words := strings.Fields(value) + return len(words) > 0 && len(words) <= 3 +} + func unsafeScriptNumericConversion(text string) bool { lowered := strings.ToLower(text) return strings.Contains(lowered, "static_cast") || diff --git a/internal/codeguard/checks/quality/quality_precision_mutation_targets.go b/internal/codeguard/checks/quality/quality_precision_mutation_targets.go index 188dbf0..f880b99 100644 --- a/internal/codeguard/checks/quality/quality_precision_mutation_targets.go +++ b/internal/codeguard/checks/quality/quality_precision_mutation_targets.go @@ -4,10 +4,14 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/devr-tools/codeguard/internal/codeguard/checks/support" ) var conventionalMutationBoundaryPattern = regexp.MustCompile(`^(accept|apply|approve|archive|clear|close|commit|deliver|download|drop|ensure|exists|fetch|import|list|notify|open|process|read|reconcile|record|run|seed|submit|sync|toggle|upload)`) +var localAccumulatorExprPattern = regexp.MustCompile(`(?i)^(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b|^\[\]|^\{\}|^make\s*\(|^(?:bytes|strings)\.buffer\b|^strings\.builder\b`) + func localMutationTargets(fn precisionFunction) map[string]struct{} { params := paramNames(fn) targets := make(map[string]struct{}) @@ -19,11 +23,41 @@ func localMutationTargets(fn precisionFunction) map[string]struct{} { if _, isParam := params[name]; isParam { continue } - targets[name] = struct{}{} + if assignmentLooksLocalAccumulator(fn, assignment) { + targets[name] = struct{}{} + } } return targets } +func assignmentLooksLocalAccumulator(fn precisionFunction, assignment support.ParsedAssignment) bool { + expr := strings.TrimSpace(assignment.Expr) + if localAccumulatorExprPattern.MatchString(expr) { + return true + } + statement := assignmentStatement(fn, assignment.Line) + if statement == "" { + return false + } + name := regexp.QuoteMeta(assignment.Name) + return regexp.MustCompile(`(?i)\b(?:const|let|var)\s+`+name+`\b.*=\s*(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b`).MatchString(statement) || + regexp.MustCompile(`(?i)\bvar\s+`+name+`\s+(?:bytes\.buffer|strings\.builder)\b`).MatchString(statement) || + regexp.MustCompile(`(?i)\bstd::(?:vector|map|set|unordered_map|unordered_set|stringstream)\b[^;\n]*\b`+name+`\b`).MatchString(statement) || + regexp.MustCompile(`\b`+name+`\s*:=\s*(?:\[\]|\{\}|make\s*\(|(?:bytes|strings)\.Buffer\b|strings\.Builder\b)`).MatchString(statement) +} + +func assignmentStatement(fn precisionFunction, line int) string { + for _, statement := range fn.Statements { + if statement.Line == line { + if strings.TrimSpace(statement.Raw) != "" { + return statement.Raw + } + return statement.Text + } + } + return "" +} + func paramNames(fn precisionFunction) map[string]struct{} { params := make(map[string]struct{}, len(fn.Params)) for _, param := range fn.Params { diff --git a/internal/codeguard/checks/quality/quality_precision_ui_conventions.go b/internal/codeguard/checks/quality/quality_precision_ui_conventions.go new file mode 100644 index 0000000..b1533ef --- /dev/null +++ b/internal/codeguard/checks/quality/quality_precision_ui_conventions.go @@ -0,0 +1,141 @@ +package quality + +import ( + "strings" + + "github.com/devr-tools/codeguard/internal/codeguard/checks/support" +) + +func isReactComponentOrHookBoundary(file string, fn precisionFunction) bool { + if !isScriptLikeSourcePath(file) { + return false + } + if isReactHookName(fn.Name) || isReactHookFile(file) { + return true + } + if isTSXLikeSourcePath(file) && isReactComponentName(fn.Name) { + return true + } + body := strings.ToLower(fn.Body) + return isTSXLikeSourcePath(file) && (strings.Contains(body, "jsx") || strings.Contains(body, "return <") || strings.Contains(body, "react.")) +} + +func isTSXLikeSourcePath(file string) bool { + lowered := strings.ToLower(file) + return strings.HasSuffix(lowered, ".tsx") || strings.HasSuffix(lowered, ".jsx") +} + +func isReactComponentName(name string) bool { + name = strings.TrimSpace(name) + if name == "" { + return false + } + first := rune(name[0]) + return first >= 'A' && first <= 'Z' +} + +func isUIConventionalAmbiguousName(file string, fn precisionFunction, name string, typ string, line int) bool { + if !isScriptLikeSourcePath(file) { + return false + } + normalized := strings.ToLower(strings.Trim(name, "_$")) + if !isUIConventionalAmbiguousToken(normalized) { + return false + } + if isReactComponentOrHookBoundary(file, fn) { + return true + } + if strings.HasPrefix(strings.ToLower(fn.Name), "on") || strings.Contains(strings.ToLower(fn.Name), "render") { + return true + } + if strings.Contains(strings.ToLower(typ), "reactnode") || strings.Contains(strings.ToLower(typ), "jsx") { + return true + } + return nearbyUICallbackStatement(fn, line) +} + +func isUIConventionalAmbiguousToken(name string) bool { + switch name { + case "value", "values", "item", "items", "data", "utils": + return true + default: + return false + } +} + +func nearbyUICallbackStatement(fn precisionFunction, line int) bool { + for _, statement := range fn.Statements { + if statement.Line < line-2 || statement.Line > line+2 { + continue + } + lowered := strings.ToLower(statement.Raw + " " + statement.Text) + if strings.Contains(lowered, ".map(") || strings.Contains(lowered, "onchange") || + strings.Contains(lowered, "onsave") || strings.Contains(lowered, "onclick") || + strings.Contains(lowered, "render") || strings.Contains(lowered, "form") { + return true + } + } + return false +} + +func isAllowedBooleanUIName(file string, fn precisionFunction, name string) bool { + if !isReactComponentOrHookBoundary(file, fn) { + return false + } + switch strings.ToLower(strings.Trim(name, "_$")) { + case "open", "loading", "active", "pending", "checked", "selected", "expanded", "collapsed": + return true + default: + return false + } +} + +func conventionalCardinalityName(name string) bool { + switch strings.ToLower(strings.Trim(name, "_$")) { + case "args", "rows", "ids", "next", "out", "props", "searchparams", "params", "item", "items", "status": + return true + default: + return strings.HasSuffix(name, "params") || + strings.HasSuffix(name, "props") || + strings.HasSuffix(name, "args") + } +} + +func moduleStatementLooksTopLevel(statement support.ParsedStatement) bool { + return statement.Indent == 0 +} + +func scriptSourceLineAtModuleScope(source string, targetIdx int) bool { + depth := 0 + lines := strings.Split(strings.ReplaceAll(source, "\r\n", "\n"), "\n") + for idx, line := range lines { + if idx == targetIdx { + return depth == 0 && len(line) == len(strings.TrimLeft(line, " \t")) + } + depth += braceDelta(maskCommentSuffix(line)) + if depth < 0 { + depth = 0 + } + } + return true +} + +func maskCommentSuffix(line string) string { + if idx := strings.Index(line, "//"); idx >= 0 { + return line[:idx] + } + return line +} + +func braceDelta(line string) int { + delta := 0 + for _, r := range line { + switch r { + case '{': + delta++ + case '}': + delta-- + } + } + return delta +} diff --git a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go index 97f4cc7..6e7b339 100644 --- a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go +++ b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go @@ -112,7 +112,7 @@ func precisionNamingFindings(env support.Context, file string, fn precisionFunct if item.name == "" { continue } - if isBooleanNameCandidate(item.name, item.typ, item.expr, fn) && !isPredicateName(item.name) { + if isBooleanNameCandidate(item.name, item.typ, item.expr, fn) && !isPredicateName(item.name) && !isAllowedBooleanUIName(file, fn, item.name) { findings = append(findings, precisionWarnFinding(env, namingBooleanNotPredicateRuleID, file, item.line, fmt.Sprintf("boolean name %q should read as a predicate such as is/has/can/should", item.name), core.ConfidenceMedium)) } @@ -416,12 +416,12 @@ func isPredicateName(name string) bool { func cardinalityMismatch(name string, typ string, expr string) bool { base := strings.ToLower(strings.Trim(name, "_$")) - if base == "" || base == "item" || base == "items" || base == "status" || strings.HasSuffix(base, "status") || strings.HasSuffix(base, "class") { + if base == "" || conventionalCardinalityName(base) || strings.HasSuffix(base, "status") || strings.HasSuffix(base, "class") { return false } plural := isPluralName(base) - collection := collectionTypePattern.MatchString(typ) || collectionExpr(expr) - scalar := scalarTypePattern.MatchString(typ) || scalarExpr(expr) + collection := collectionTypePattern.MatchString(typ) + scalar := scalarTypePattern.MatchString(typ) if plural && scalar && !collection { return true } @@ -435,27 +435,6 @@ func isPluralName(name string) bool { return strings.HasSuffix(name, "s") && !strings.HasSuffix(name, "ss") && !strings.HasSuffix(name, "us") } -func collectionExpr(expr string) bool { - expr = strings.TrimSpace(strings.ToLower(expr)) - return strings.HasPrefix(expr, "[]") || strings.HasPrefix(expr, "[") || strings.HasPrefix(expr, "map[") || - strings.HasPrefix(expr, "make([]") || strings.Contains(expr, "new map") || strings.Contains(expr, "new set") || - strings.Contains(expr, "array<") || strings.Contains(expr, "list<") || strings.Contains(expr, "vector<") -} - -func scalarExpr(expr string) bool { - expr = strings.TrimSpace(strings.TrimSuffix(expr, ";")) - if expr == "" { - return false - } - if booleanExprPattern.MatchString(expr) { - return true - } - if expr[0] == '"' || expr[0] == '\'' || (expr[0] >= '0' && expr[0] <= '9') { - return true - } - return false -} - func implementationLeakName(name string) bool { words := splitIdentifierWords(name) if len(words) <= 1 { diff --git a/tests/checks/design_local_abstraction_test.go b/tests/checks/design_local_abstraction_test.go index 1cbac89..3d622f7 100644 --- a/tests/checks/design_local_abstraction_test.go +++ b/tests/checks/design_local_abstraction_test.go @@ -132,3 +132,34 @@ func TestDesignLocalAbstractionAdditionalLanguages(t *testing.T) { cppReport := runDesignLocalScan(t, designLocalConfig(dir, "cpp")) assertFindingRulePresent(t, cppReport, "Design Patterns", "design.persistence-model-leak") } + +func TestDesignPersistenceModelLeakAllowsGeneratedPrismaEnums(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps", "web", "app", "contracts", "_components", "contracts-filters.ts"), strings.Join([]string{ + "import { ContractStatus, ContractType } from '@prisma/client';", + "export const statusLabels: Record = {", + " DRAFT: 'Draft',", + "} as Record;", + "export type ContractFilter = { status?: ContractStatus; type?: ContractType };", + }, "\n")) + writeFile(t, filepath.Join(dir, "apps", "web", "app", "api", "contracts", "route.ts"), strings.Join([]string{ + "export type ContractModel = { id: string };", + "export async function GET() {", + " return Response.json({ ok: true });", + "}", + }, "\n")) + + report := runDesignLocalScan(t, designLocalConfig(dir, "typescript")) + + assertFindingRulePresent(t, report, "Design Patterns", "design.persistence-model-leak") + for _, section := range report.Sections { + if section.Name != "Design Patterns" { + continue + } + for _, finding := range section.Findings { + if finding.RuleID == "design.persistence-model-leak" && strings.Contains(finding.Path, "contracts-filters.ts") { + t.Fatalf("generated Prisma enum import should not leak persistence model: %+v", finding) + } + } + } +} diff --git a/tests/checks/function_hidden_mutation_noise_test.go b/tests/checks/function_hidden_mutation_noise_test.go index d45410a..0bca437 100644 --- a/tests/checks/function_hidden_mutation_noise_test.go +++ b/tests/checks/function_hidden_mutation_noise_test.go @@ -114,6 +114,24 @@ func TestFunctionHiddenMutationStillWarnsForCollaboratorMutationWithLocalPayload assertFindingRulePresent(t, report, "Code Quality", "function.hidden-mutation") } +func TestFunctionHiddenMutationStillWarnsForLocalCollaboratorConstruction(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "mutation.ts"), strings.Join([]string{ + "export function prepareDigest(input: Input) {", + " const repo = getRepository();", + " repo.save(input);", + " return input;", + "}", + "function getRepository(): Repository { throw new Error('test'); }", + "interface Repository { save(input: Input): void }", + "interface Input { id: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRulePresent(t, report, "Code Quality", "function.hidden-mutation") +} + func TestFunctionHiddenMutationAllowsNextRouteHandlerNames(t *testing.T) { cases := []struct { name string diff --git a/tests/checks/quality_ui_false_positive_hardening_test.go b/tests/checks/quality_ui_false_positive_hardening_test.go new file mode 100644 index 0000000..3478f8c --- /dev/null +++ b/tests/checks/quality_ui_false_positive_hardening_test.go @@ -0,0 +1,168 @@ +package checks_test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/devr-tools/codeguard/pkg/codeguard" +) + +func TestQualityAmbiguousNameAllowsConventionalUIParams(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/claims/claim-classification-fields.tsx"), strings.Join([]string{ + "export function ClaimClassificationFields() {", + " function onChange(value: string) {", + " return value.trim();", + " }", + " function renderItem(item: Item) {", + " return {item.label};", + " }", + " return ;", + "}", + "interface Item { label: string }", + "declare function Field(props: unknown): unknown;", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.ambiguous-name") +} + +func TestFunctionCommandQueryMixAllowsReactAndNextBoundaries(t *testing.T) { + cases := []struct { + name string + file string + source []string + }{ + { + name: "react component", + file: "apps/web/app/contracts/contract-detail-classification.tsx", + source: []string{ + "export function ContractDetailClassification() {", + " async function onSave() {", + " await repo.save({ ok: true });", + " }", + " return ;", + "}", + }, + }, + { + name: "react hook", + file: "apps/web/app/contracts/use-contract-classification.ts", + source: []string{ + "export function useContractClassification(repo: Repository) {", + " async function saveClassification() {", + " await repo.save({ ok: true });", + " }", + " return { saveClassification };", + "}", + "interface Repository { save(input: unknown): Promise }", + }, + }, + { + name: "next route", + file: "apps/web/app/api/files/upload/route.ts", + source: []string{ + "export async function POST(request: Request) {", + " await storage.upload(request);", + " return Response.json({ ok: true });", + "}", + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, tc.file), strings.Join(tc.source, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.command-query-mix") + }) + } +} + +func TestQualityDuplicatedKnowledgeSkipsDisplayStringsAndIncludesLiteral(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/claims/labels.tsx"), strings.Join([]string{ + "export function Labels() {", + " return
Status
;", + "}", + "export const first = 'claim_status_code';", + "export const second = 'claim_status_code';", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + finding := firstFindingForRule(t, report, "Code Quality", "quality.duplicated-knowledge") + if !strings.Contains(finding.Message, "'claim_status_code'") { + t.Fatalf("expected duplicated literal in message, got %q", finding.Message) + } +} + +func TestNamingCardinalityMismatchAllowsFrameworkConventions(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/okrs/use-kr-drag.ts"), strings.Join([]string{ + "export function useKrDrag(args: DragArgs, props: Props, searchParams: URLSearchParams, next: string, out: Result) {", + " const ids = next;", + " const rows = props.row;", + " return { args, props, searchParams, ids, rows, out };", + "}", + "interface DragArgs { id: string }", + "interface Props { row: string }", + "interface Result { id: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "naming.cardinality-mismatch") +} + +func TestQualityMutableGlobalStateIgnoresReactLocalBindings(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/claims/claim-classification-fields.tsx"), strings.Join([]string{ + "export function ClaimClassificationFields() {", + " let value = '';", + " const data = new Map();", + " data.set('classification_status', value);", + " return
{value}
;", + "}", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.mutable-global-state") +} + +func TestNamingBooleanNotPredicateAllowsUIPropsAndHandlers(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/contracts/contract-detail-classification.tsx"), strings.Join([]string{ + "export function ContractDetailClassification(open: boolean, loading: boolean, active: boolean, pending: boolean, onSave: () => void) {", + " if (open && active && !loading && !pending) {", + " onSave();", + " }", + " return ;", + "}", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "naming.boolean-not-predicate") +} + +func firstFindingForRule(t *testing.T, report codeguard.Report, sectionName string, ruleID string) codeguard.Finding { + t.Helper() + for _, section := range report.Sections { + if section.Name != sectionName { + continue + } + for _, finding := range section.Findings { + if finding.RuleID == ruleID { + return finding + } + } + } + t.Fatalf("rule %q not found in section %q", ruleID, sectionName) + return codeguard.Finding{} +} From 62643da4d57a6eb7fde0926de0121333e605902f Mon Sep 17 00:00:00 2001 From: Alex Wilkerson John Date: Mon, 27 Jul 2026 20:03:52 -0400 Subject: [PATCH 4/4] fix: satisfy lint for false positive hardening --- internal/codeguard/checks/design/local_abstraction.go | 6 +++--- .../checks/quality/quality_precision_workstreams_cd.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/codeguard/checks/design/local_abstraction.go b/internal/codeguard/checks/design/local_abstraction.go index c9808fd..01889d5 100644 --- a/internal/codeguard/checks/design/local_abstraction.go +++ b/internal/codeguard/checks/design/local_abstraction.go @@ -154,11 +154,11 @@ func allowedGeneratedPersistenceEnumLine(line string) bool { return false } open := strings.Index(line, "{") - close := strings.Index(line, "}") - if open < 0 || close <= open { + closeBrace := strings.Index(line, "}") + if open < 0 || closeBrace <= open { return false } - for _, part := range strings.Split(line[open+1:close], ",") { + for _, part := range strings.Split(line[open+1:closeBrace], ",") { name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(part), "type ")) if name == "" { continue diff --git a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go index 6e7b339..076398e 100644 --- a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go +++ b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go @@ -116,7 +116,7 @@ func precisionNamingFindings(env support.Context, file string, fn precisionFunct findings = append(findings, precisionWarnFinding(env, namingBooleanNotPredicateRuleID, file, item.line, fmt.Sprintf("boolean name %q should read as a predicate such as is/has/can/should", item.name), core.ConfidenceMedium)) } - if cardinalityMismatch(item.name, item.typ, item.expr) { + if cardinalityMismatch(item.name, item.typ) { findings = append(findings, precisionWarnFinding(env, namingCardinalityMismatchRuleID, file, item.line, fmt.Sprintf("identifier %q has plural/singular wording that conflicts with its value shape", item.name), core.ConfidenceMedium)) } @@ -414,7 +414,7 @@ func isPredicateName(name string) bool { return false } -func cardinalityMismatch(name string, typ string, expr string) bool { +func cardinalityMismatch(name string, typ string) bool { base := strings.ToLower(strings.Trim(name, "_$")) if base == "" || conventionalCardinalityName(base) || strings.HasSuffix(base, "status") || strings.HasSuffix(base, "class") { return false