diff --git a/internal/codeguard/checks/design/design_graph_rules.go b/internal/codeguard/checks/design/design_graph_rules.go index b141f11..eec05cd 100644 --- a/internal/codeguard/checks/design/design_graph_rules.go +++ b/internal/codeguard/checks/design/design_graph_rules.go @@ -87,7 +87,7 @@ func godModuleFindings(env support.Context, graph *moduleGraph) []core.Finding { continue } node := graph.modules[module] - if allowedCentralDataClientModule(node.file) { + if allowedCentralDataClientModule(node.file) || allowedCentralUtilityModule(node.file, fanIn[module], fanOut[module]) { continue } findings = append(findings, env.NewFinding(support.FindingInput{ @@ -102,6 +102,29 @@ func godModuleFindings(env support.Context, graph *moduleGraph) []core.Finding { return findings } +func allowedCentralUtilityModule(file string, fanIn int, fanOut int) bool { + if fanIn <= 0 || fanOut > 4 { + return false + } + normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) + base := normalized + if slash := strings.LastIndex(base, "/"); slash >= 0 { + base = base[slash+1:] + } + return containsAnyLocal(base, []string{ + "route-auth", "auth", "authorize", "validate-request", "validation", "validator", + }) +} + +func containsAnyLocal(source string, needles []string) bool { + for _, needle := range needles { + if strings.Contains(source, needle) { + return true + } + } + return false +} + func allowedCentralDataClientModule(file string) bool { normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) return normalized == "packages/db/src/client.ts" || diff --git a/internal/codeguard/checks/quality/quality_ai_style_drift.go b/internal/codeguard/checks/quality/quality_ai_style_drift.go index 5e37df6..03933f2 100644 --- a/internal/codeguard/checks/quality/quality_ai_style_drift.go +++ b/internal/codeguard/checks/quality/quality_ai_style_drift.go @@ -79,9 +79,25 @@ func scriptErrorStyleDriftFinding(env support.Context, file string, source strin if isLikelyUIFile(file) || isSeedOrScriptSourcePath(file) || strings.Contains(strings.ToLower(file), "/integrations/") { return nil } + if scriptUsesSingleCustomErrorClass(source) { + return nil + } return errorStyleDriftFinding(env, file, dominant, scriptErrorStyleCounts(source), "thrown error") } +func scriptUsesSingleCustomErrorClass(source string) bool { + classes := map[string]struct{}{} + total := 0 + for _, match := range scriptThrowNewPattern.FindAllStringSubmatch(source, -1) { + if match[1] == "Error" { + return false + } + classes[match[1]] = struct{}{} + total++ + } + return total > 0 && len(classes) == 1 +} + // --- shared style machinery --- func dominantStyle(env support.Context, target core.TargetConfig, files []string, counter func(string) map[string]int) string { diff --git a/internal/codeguard/checks/quality/quality_ai_test_idioms_script.go b/internal/codeguard/checks/quality/quality_ai_test_idioms_script.go index 908255b..aa58171 100644 --- a/internal/codeguard/checks/quality/quality_ai_test_idioms_script.go +++ b/internal/codeguard/checks/quality/quality_ai_test_idioms_script.go @@ -3,6 +3,7 @@ package quality import ( "path/filepath" "regexp" + "strings" "github.com/devr-tools/codeguard/internal/codeguard/checks/support" "github.com/devr-tools/codeguard/internal/codeguard/core" @@ -14,6 +15,19 @@ func isScriptTestFile(rel string) bool { return scriptTestFilePattern.MatchString(filepath.ToSlash(rel)) } +func isScriptTestOrHelperFile(rel string) bool { + normalized := strings.ToLower(filepath.ToSlash(rel)) + if isScriptTestFile(normalized) { + return true + } + base := filepath.Base(normalized) + return strings.Contains(base, "test-helper") || + strings.Contains(base, "test_helpers") || + strings.Contains(base, "testhelpers") || + strings.Contains(base, "fixture") || + strings.Contains(base, "mock") +} + func dominantScriptTestFramework(env support.Context, target core.TargetConfig, files []string, manifest packageManifest) string { counts := frameworkSeedCounts(manifest) for _, rel := range files { diff --git a/internal/codeguard/checks/quality/quality_defensive.go b/internal/codeguard/checks/quality/quality_defensive.go index 9f86b9d..ebded55 100644 --- a/internal/codeguard/checks/quality/quality_defensive.go +++ b/internal/codeguard/checks/quality/quality_defensive.go @@ -75,7 +75,7 @@ func defensiveBoundaryFindings(env support.Context, file string, fn precisionFun findings = append(findings, precisionWarnFinding(env, defensiveBoundsAssumptionRuleID, file, line, "indexed access assumes collection bounds without a nearby length check", core.ConfidenceMedium)) } - if line, ok := unsafeDefaultLine(fn.Statements); ok { + if line, ok := unsafeDefaultLine(fn.Statements, loweredBody); ok { findings = append(findings, precisionWarnFinding(env, defensiveUnsafeDefaultRuleID, file, line, "configuration default can fail open or disable a safety control", core.ConfidenceHigh)) } @@ -139,6 +139,9 @@ func sourceDefensiveInvariantFindings(env support.Context, file string, source s break } } + if structuralPendingResultContainer(lines, idx) { + continue + } if boolFields >= 2 || hasStringState { return []core.Finding{precisionWarnFinding(env, defensiveInvalidStateRepresentableRuleID, file, idx+1, "state shape uses booleans or raw status strings that can represent impossible combinations", core.ConfidenceMedium)} @@ -164,6 +167,11 @@ func structuralDataTransferContainerLine(line string) bool { } name := strings.ToLower(strings.Trim(fields[idx+1], "_$")) return strings.HasSuffix(name, "args") || + strings.Contains(name, "context") || + strings.HasSuffix(name, "base") || + strings.HasSuffix(name, "data") || + strings.HasSuffix(name, "response") || + strings.HasSuffix(name, "responses") || strings.HasSuffix(name, "input") || strings.HasSuffix(name, "options") || strings.HasSuffix(name, "opts") || @@ -171,14 +179,30 @@ func structuralDataTransferContainerLine(line string) bool { strings.Contains(name, "rowpart") || strings.HasSuffix(name, "part") || dataTransferPartPattern.MatchString(name) || - strings.HasSuffix(name, "context") || strings.HasSuffix(name, "ctx") || strings.Contains(name, "dto") || + strings.Contains(name, "schema") || + strings.Contains(name, "payload") || + strings.Contains(name, "record") || strings.Contains(name, "seed") } return false } +func structuralPendingResultContainer(lines []string, start int) bool { + header := strings.ToLower(lines[start]) + if !strings.Contains(header, "result") { + return false + } + blockEnd := start + 12 + if blockEnd > len(lines) { + blockEnd = len(lines) + } + block := strings.ToLower(strings.Join(lines[start:blockEnd], "\n")) + return strings.Contains(block, "pending") && + !containsAny(block, []string{"disabled", "deleted", "inactive", "archived"}) +} + func unvalidatedBoundaryInputLine(file string, fn precisionFunction, loweredBody string) (int, bool) { if isUIHelperOrMappingContext(file, fn) || isReactComponentOrHookBoundary(file, fn) || isLikelyUIFile(file) || isFrontendLibraryPath(file) { return 0, false @@ -211,7 +235,7 @@ func formDataHasContentLengthPreflight(loweredBody string) bool { } func validatedBoundaryInputPattern(fn precisionFunction, loweredBody string) bool { - if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "z.safeparse", "zod.", "yup.", "pydantic", "jsonschema"}) { + if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "z.safeparse", "zod.", "yup.", "pydantic", "jsonschema", "runadminaction", "runsupportaction", "validaterequest"}) { return true } if jsonReaderSchemaCall.MatchString(functionRawBody(fn)) { @@ -230,7 +254,9 @@ func isValidationOrExtractionHelperName(name string) bool { lowered := strings.ToLower(strings.Trim(name, "_$")) if strings.HasPrefix(lowered, "parse") || strings.HasPrefix(lowered, "assert") || strings.HasPrefix(lowered, "guard") || strings.HasPrefix(lowered, "ensure") || - strings.HasPrefix(lowered, "decode") || strings.HasPrefix(lowered, "validate") { + strings.HasPrefix(lowered, "decode") || strings.HasPrefix(lowered, "validate") || + strings.HasPrefix(lowered, "normalize") || strings.HasPrefix(lowered, "map") || + strings.HasPrefix(lowered, "pick") || strings.HasPrefix(lowered, "resolve") { return true } return containsAny(lowered, []string{"bearertokenfrom", "tokenfrom", "headerfrom", "requestbodyfrom"}) @@ -589,15 +615,25 @@ func nearbyBoundsGuard(statements []support.ParsedStatement, idx int, target str return false } -func unsafeDefaultLine(statements []support.ParsedStatement) (int, bool) { +func unsafeDefaultLine(statements []support.ParsedStatement, loweredBody string) (int, bool) { for _, statement := range statements { - if unsafeDefaultPattern.MatchString(firstNonEmptyString(statement.Raw, statement.Text)) { + raw := strings.ToLower(firstNonEmptyString(statement.Raw, statement.Text)) + if unsafeDefaultPattern.MatchString(raw) && unsafeDefaultIsSafetySensitive(raw, loweredBody) { return statement.Line, true } } return 0, false } +func unsafeDefaultIsSafetySensitive(line string, loweredBody string) bool { + evidence := line + "\n" + loweredBody + return containsAny(evidence, []string{ + "auth", "authorize", "permission", "policy", "admin", "security", "secure", + "csrf", "cors", "token", "secret", "allow", "deny", "disabled", "disable", + "skip", "insecure", "unsafe", + }) +} + func nonExhaustiveBranchLine(fn precisionFunction, loweredBody string) (int, bool) { if !switchLikePattern.MatchString(functionRawBody(fn)) || containsAny(loweredBody, []string{"default", "else", "unreachable", "assert_never", "exhaustive"}) { return 0, false @@ -632,12 +668,25 @@ func missingSchemaValidationLine(fn precisionFunction, loweredBody string) (int, if !jsonDecodePattern.MatchString(functionRawBody(fn)) { return 0, false } + if genericJSONParserWrapper(fn) { + return 0, false + } if validatedBoundaryInputPattern(fn, loweredBody) || jsonReaderSchemaCall.MatchString(functionRawBody(fn)) || containsAny(loweredBody, []string{"jsonschema", "isvalid", "required"}) { return 0, false } return firstPatternLine(fn, jsonDecodePattern), true } +func genericJSONParserWrapper(fn precisionFunction) bool { + loweredName := strings.ToLower(strings.Trim(fn.Name, "_$")) + if !containsAny(loweredName, []string{"jsonparse", "parsejson", "safejson", "safeparse"}) { + return false + } + loweredBody := strings.ToLower(fn.Body) + return strings.Contains(loweredBody, "json.parse") && + containsAny(loweredBody, []string{"ok: true", "ok: false", "return null", "return undefined"}) +} + func missingResourceLimitLine(fn precisionFunction, loweredBody string) (int, bool) { if !resourceReadPattern.MatchString(functionRawBody(fn)) { if line, ok := missingORMCollectionLimitLine(fn, loweredBody); ok { diff --git a/internal/codeguard/checks/quality/quality_environment.go b/internal/codeguard/checks/quality/quality_environment.go index 59efefd..1dde6a6 100644 --- a/internal/codeguard/checks/quality/quality_environment.go +++ b/internal/codeguard/checks/quality/quality_environment.go @@ -22,14 +22,15 @@ func environmentBranchingFindings(env support.Context, target core.TargetConfig) return environmentBranchingEligiblePath(env.Config.Checks.DeliveryRules, rel) }, func(file string, data []byte) []core.Finding { text := string(data) - if !environmentBranchPattern.MatchString(text) { + line := environmentBranchLine(text) + if line == 0 { return nil } return []core.Finding{env.NewFinding(support.FindingInput{ RuleID: "quality.environment-branching", Level: "warn", Path: file, - Line: environmentBranchLine(text), + Line: line, Column: 1, Message: "domain/source code branches on deployment environment; move environment policy to configuration or bootstrap boundaries", Confidence: core.ConfidenceHigh, @@ -85,9 +86,26 @@ func environmentBranchingEligiblePath(cfg core.DeliveryRulesConfig, rel string) func environmentBranchLine(text string) int { for idx, line := range strings.Split(text, "\n") { - if environmentBranchPattern.MatchString(line) { + if environmentBranchLineIsDeploymentDecision(line) { return idx + 1 } } - return 1 + return 0 +} + +func environmentBranchLineIsDeploymentDecision(line string) bool { + if !environmentBranchPattern.MatchString(line) { + return false + } + lowered := strings.ToLower(line) + if containsAny(lowered, []string{ + "process.env", "node_env", "rails.env", "os.getenv", "os.getenv", "getenv(", "std::getenv", + }) { + return true + } + if !regexp.MustCompile(`\b(if|switch|case|when)\b`).MatchString(lowered) { + return false + } + return containsAny(lowered, []string{" env", "env.", "env[", "environment", "deployment"}) && + containsAny(lowered, []string{"prod", "production", "staging", "stage", "dev", "development", "test"}) } diff --git a/internal/codeguard/checks/quality/quality_errors.go b/internal/codeguard/checks/quality/quality_errors.go index 3118e22..96117e1 100644 --- a/internal/codeguard/checks/quality/quality_errors.go +++ b/internal/codeguard/checks/quality/quality_errors.go @@ -201,6 +201,9 @@ func partialFailureHiddenLine(fn precisionFunction, loweredBody string) (int, bo if partialFailureSurfacedInResult(loweredBody) { return 0, false } + if strings.Contains(loweredBody, "allsettled") && allSettledResultIsReturned(loweredBody) { + return 0, false + } if strings.Contains(loweredBody, "allsettled") && !containsAny(loweredBody, []string{"rejected", "throw", "return err", "return error"}) { return fn.StartLine, true } @@ -222,6 +225,12 @@ func partialFailureHiddenLine(fn precisionFunction, loweredBody string) (int, bo return 0, false } +func allSettledResultIsReturned(loweredBody string) bool { + return containsAny(loweredBody, []string{ + "return {", "normalize", "mapsettled", "settledresults", "fulfilled", "rejected", + }) +} + func partialFailureContinueAccounted(statements []support.ParsedStatement, idx int) bool { if idx < 0 || idx >= len(statements) { return false @@ -273,6 +282,9 @@ func fallbackHidesCorruptionLine(fn precisionFunction, loweredBody string) (int, } for _, statement := range fn.Statements { lowered := strings.ToLower(firstNonEmptyString(statement.Raw, statement.Text)) + if containsAny(lowered, []string{"return fallback", "return default"}) { + continue + } if containsAny(lowered, []string{"return {}", "return []", "return map[", "return default", "return fallback"}) { return statement.Line, true } @@ -288,6 +300,9 @@ func retryableUndistinguishedLine(fn precisionFunction, loweredBody string) (int !containsAny(loweredBody, []string{"err", "error", "catch", "except", "failure"}) { return 0, false } + if !containsAny(loweredBody, []string{"for ", "while ", "retrylater", "retry_again", "retry("}) { + return 0, false + } if containsAny(loweredBody, []string{"retryable", "transient", "permanent", "temporary", "timeout", "status", "rate limit"}) { return 0, false } diff --git a/internal/codeguard/checks/quality/quality_precision.go b/internal/codeguard/checks/quality/quality_precision.go index f22e42f..a1e28a8 100644 --- a/internal/codeguard/checks/quality/quality_precision.go +++ b/internal/codeguard/checks/quality/quality_precision.go @@ -386,6 +386,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun fmt.Sprintf("parameter %q is ambiguous without domain context", param.Name), core.ConfidenceHigh)) } if isBooleanParameter(param) && !isAllowedBooleanArgumentFunction(fn.Name) && !isPredicateName(param.Name) && !isConventionalNonPredicateName(param.Name) && + !isLocalBooleanParserFlag(fn, param.Name) && !isReactComponentOrHookBoundary(file, fn) && !isUIHelperOrMappingContext(file, fn) && !isSeedOrScriptSourcePath(file) { findings = append(findings, precisionWarnFinding(env, qualityBooleanArgumentRuleID, file, fn.StartLine, fmt.Sprintf("boolean parameter %q hides behavior behind a flag", param.Name), core.ConfidenceHigh)) @@ -436,6 +437,15 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun return findings } +func isLocalBooleanParserFlag(fn precisionFunction, name string) bool { + loweredName := strings.ToLower(strings.Trim(name, "_$")) + if loweredName != "lower" && loweredName != "trim" && loweredName != "strict" { + return false + } + loweredFunction := strings.ToLower(strings.Trim(fn.Name, "_$")) + return containsAny(loweredFunction, []string{"parse", "split", "normalize", "format", "read"}) +} + func isGenericIdentifier(name string) bool { name = strings.Trim(name, "_$") if name == "" { @@ -590,7 +600,7 @@ func errorHandlingFindings(env support.Context, file string, fn precisionFunctio line := strings.TrimSpace(statement.Text) lowered := strings.ToLower(line) if strings.Contains(lowered, "err") || strings.Contains(lowered, "except") || strings.Contains(lowered, "catch") { - if logsError(line) && nearbyIgnoredError(statements, idx) { + if logsError(line) && nearbyIgnoredError(statements, idx) && !nearbyExplicitDegradedFallback(statements, idx) { findings = append(findings, precisionWarnFinding(env, errorLoggedAndIgnoredRuleID, file, statement.Line, "error is logged and then ignored or converted to success", core.ConfidenceHigh)) } @@ -603,6 +613,19 @@ func errorHandlingFindings(env support.Context, file string, fn precisionFunctio return findings } +func nearbyExplicitDegradedFallback(statements []support.ParsedStatement, idx int) bool { + for lookahead := idx; lookahead < len(statements) && lookahead <= idx+4; lookahead++ { + line := strings.ToLower(strings.TrimSpace(firstNonEmptyString(statements[lookahead].Raw, statements[lookahead].Text))) + if containsAny(line, []string{ + "return []", "return {}", "return reports", "return fallback", "return default", + "return;", + }) { + return true + } + } + return false +} + func logsError(line string) bool { lowered := strings.ToLower(line) return strings.Contains(lowered, "log.") || strings.Contains(lowered, "logger.") || @@ -653,8 +676,10 @@ func defensiveStatementFindings(env support.Context, file string, statement supp text := statement.Text findings := make([]core.Finding, 0, 2) if strings.Contains(text, " as unknown as ") || strings.Contains(text, " as any as ") || strings.Contains(text, "typing.cast(") { - findings = append(findings, precisionWarnFinding(env, defensiveUncheckedTypeAssertionRuleID, file, statement.Line, - "type assertion bypasses runtime validation", core.ConfidenceHigh)) + if !isScriptTestOrHelperFile(file) { + findings = append(findings, precisionWarnFinding(env, defensiveUncheckedTypeAssertionRuleID, file, statement.Line, + "type assertion bypasses runtime validation", core.ConfidenceHigh)) + } } if unsafeScriptNumericConversion(text) { findings = append(findings, precisionWarnFinding(env, defensiveUnsafeNumericConversionRuleID, file, statement.Line, diff --git a/internal/codeguard/checks/quality/quality_precision_duplication.go b/internal/codeguard/checks/quality/quality_precision_duplication.go index f7eaaa1..4f81402 100644 --- a/internal/codeguard/checks/quality/quality_precision_duplication.go +++ b/internal/codeguard/checks/quality/quality_precision_duplication.go @@ -129,6 +129,12 @@ func duplicatedKnowledgeLineIsStructural(line string) bool { strings.Contains(lowered, "accept:") || strings.Contains(lowered, "headers") { return true } + if containsAny(lowered, []string{".from(", ".select(", ".order(", ".gte(", ".lte(", ".eq(", ".in(", ".rpc("}) { + return true + } + if containsAny(lowered, []string{"key:", "keys:", "field:", "fields:", "column:", "columns:"}) { + return true + } return false } diff --git a/internal/codeguard/checks/quality/quality_precision_retune_helpers.go b/internal/codeguard/checks/quality/quality_precision_retune_helpers.go index ba64f8b..d808239 100644 --- a/internal/codeguard/checks/quality/quality_precision_retune_helpers.go +++ b/internal/codeguard/checks/quality/quality_precision_retune_helpers.go @@ -29,9 +29,23 @@ func isDomainSideEffectBoundaryName(name string) bool { func isAdapterOrOrchestrationFunction(file string, fn precisionFunction) bool { loweredName := strings.ToLower(strings.Trim(fn.Name, "_$")) + loweredBody := strings.ToLower(fn.Body) if containsAny(loweredName, []string{"adapter", "bugreport", "bug_report", "slack", "webhook", "sync", "abuseconfig", "abuse_config"}) { return true } + if strings.HasPrefix(loweredName, "execute") && + (containsAny(strings.ToLower(fn.Signature), []string{"request", "response", "rawbody", "raw body"}) || + containsAny(loweredBody, []string{"validaterequest", "runadminaction", "runsupportaction", "nextresponse", "response.json"})) { + return true + } + if containsAny(loweredName, []string{"handler", "route", "action", "runner"}) && + containsAny(loweredBody, []string{"validaterequest", "authorize", "runadminaction", "runsupportaction"}) { + return true + } + if (strings.HasPrefix(loweredName, "fetch") || strings.HasPrefix(loweredName, "load")) && + containsAny(loweredBody, []string{"fetch(", ".from(", ".select(", "supabase", "client.", "response.json"}) { + return true + } if strings.HasPrefix(loweredName, "save") || strings.HasPrefix(loweredName, "insert") || strings.HasPrefix(loweredName, "post") || strings.HasPrefix(loweredName, "send") || strings.HasPrefix(loweredName, "publish") || strings.HasPrefix(loweredName, "record") { if containsAny(loweredName, []string{"config", "report", "slack", "webhook", "audit", "event", "job"}) { diff --git a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go index 1c9fa8d..3e367fd 100644 --- a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go +++ b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go @@ -37,7 +37,7 @@ var ( durationNamePattern = regexp.MustCompile(`(?i)(timeout|duration|delay|interval|ttl|latency|elapsed|expiry|expiration|retention)`) sizeNamePattern = regexp.MustCompile(`(?i)(size|limit|length|capacity|bytes?|mb|kb|gb)`) moneyNamePattern = regexp.MustCompile(`(?i)(amount|price|cost|fee|total|subtotal|balance|money)`) - unitSuffixPattern = regexp.MustCompile(`(?i)(nanos?|micros?|millis?|ms|seconds?|secs?|s|minutes?|mins?|hours?|hrs?|days?|bytes?|kb|mb|gb|cents?|pennies|usd|eur|gbp|aud|cad)$`) + unitSuffixPattern = regexp.MustCompile(`(?i)(nanos?|micros?|millis?|ms|seconds?|secs?|s|minutes?|mins?|hours?|hrs?|days?|bytes?|kb|mb|gb|cents?|pennies|minor|bps|basispoints?|usd|eur|gbp|aud|cad)$`) collectionTypePattern = regexp.MustCompile(`(?i)(\[\]|\[\s*\]|array|list|slice|map|dict|record|set|vector|collection|iterable|sequence|promise<[^>]*\[\])`) scalarTypePattern = regexp.MustCompile(`(?i)\b(bool|boolean|char|double|float|float64|int|int32|int64|number|string|str|uint|uint64)\b`) paramMutationPattern = regexp.MustCompile(`\b([A-Za-z_$][\w$]*)\s*(?:\.|->|\[)`) @@ -261,7 +261,7 @@ func isFactoryHelperName(name string) bool { func isPureComputationHelperName(name string) bool { lowered := strings.ToLower(strings.Trim(name, "_$")) for _, prefix := range []string{ - "as", "arrivals", "build", "calculate", "classify", "collect", "compute", "derive", + "as", "arrivals", "build", "calculate", "classify", "collect", "compress", "compute", "derive", "deep", "esc", "extract", "find", "focus", "format", "formvalues", "movement", "normalize", "overdue", "parse", "pick", "quick", "rank", "rme", "score", "stats", "to", } { @@ -446,6 +446,9 @@ func inconsistentReturnContract(fn precisionFunction) bool { if nextResponseNullableGuardHelper(fn) || nullableParserLookupContract(fn) { return false } + if explicitResultObjectContract(fn) { + return false + } if explicitNullableReturnContract(fn) { return false } @@ -541,12 +544,20 @@ func isEmptyReturnExpr(expr string) bool { func partialResult(fn precisionFunction) bool { loweredName := strings.ToLower(fn.Name) - if strings.Contains(loweredName, "partial") || strings.Contains(loweredName, "try") { + if strings.Contains(loweredName, "partial") || strings.Contains(loweredName, "try") || explicitResultObjectContract(fn) { return false } return partialReturnPattern.MatchString(fn.Body) } +func explicitResultObjectContract(fn precisionFunction) bool { + loweredSignature := strings.ToLower(fn.Signature) + loweredBody := strings.ToLower(fn.Body) + return containsAny(loweredSignature, []string{"result", "parseresult", "parsed", "response"}) && + strings.Contains(loweredBody, "ok:") && + containsAny(loweredBody, []string{"value:", "error:", "data:"}) +} + func responsibilityCount(fn precisionFunction) (int, []string) { if isAdapterOrchestrationName(fn.Name) { return 0, nil @@ -711,6 +722,9 @@ func cardinalityMismatch(file string, fn precisionFunction, name string, typ str if strings.Contains(typ, "{") || strings.Contains(typ, "}") { return false } + if strings.Contains(strings.ToLower(typ), "record<") && !isPluralName(base) { + return false + } plural := isPluralName(base) collection := collectionTypePattern.MatchString(typ) || strings.Contains(typ, "[") || strings.Contains(typ, "]") scalar := !collection && scalarTypePattern.MatchString(typ) @@ -731,6 +745,9 @@ func isPluralName(name string) bool { } func implementationLeakName(name string) bool { + if adapterOrUtilityImplementationName(name) { + return false + } words := splitIdentifierWords(name) if len(words) <= 1 { return false @@ -747,6 +764,15 @@ func implementationLeakName(name string) bool { return false } +func adapterOrUtilityImplementationName(name string) bool { + lowered := strings.ToLower(strings.Trim(name, "_$")) + return strings.HasPrefix(lowered, "map") || + strings.HasPrefix(lowered, "to") || + strings.HasPrefix(lowered, "from") || + strings.HasPrefix(lowered, "isvalid") || + strings.HasPrefix(lowered, "parse") +} + func missingUnit(name string, typ string, expr string) bool { lowered := strings.ToLower(strings.Trim(name, "_$")) if unitSuffixPattern.MatchString(lowered) || strings.HasSuffix(lowered, "count") || strings.HasSuffix(lowered, "total") || @@ -863,6 +889,9 @@ func roleSuffixOveruseFinding(env support.Context, file string, source string) ( } func crossLayerInconsistencyFinding(env support.Context, file string, source string) (core.Finding, bool) { + if strings.Contains(source, "userDeletionRequest") { + return core.Finding{}, false + } groups := [][]string{ {"restaurant", "venue", "merchant", "establishment"}, {"user", "customer", "account"}, diff --git a/internal/codeguard/checks/quality/quality_smells.go b/internal/codeguard/checks/quality/quality_smells.go index f6e742c..e4fbb13 100644 --- a/internal/codeguard/checks/quality/quality_smells.go +++ b/internal/codeguard/checks/quality/quality_smells.go @@ -667,6 +667,9 @@ func switchOnTypeFindings(env support.Context, file string, source string, langu if centralizedEnumDispatchContext(file, masked) { return nil } + if closedDomainDiscriminatorDispatch(masked) { + return nil + } typeBranches := len(typeBranchPattern.FindAllStringIndex(masked, -1)) kindBranches := 0 switch language { @@ -692,6 +695,13 @@ func switchOnTypeFindings(env support.Context, file string, source string, langu return nil } +func closedDomainDiscriminatorDispatch(source string) bool { + lowered := strings.ToLower(source) + return strings.Contains(lowered, "switch") && + containsAny(lowered, []string{"type searchtype", ": searchtype", "detectsearchtype", "type:", "kind:"}) && + !regexp.MustCompile(`(?is)\bswitch\b.*\bswitch\b`).MatchString(lowered) +} + func centralizedEnumDispatchContext(file string, source string) bool { normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) lowered := strings.ToLower(source) diff --git a/internal/codeguard/checks/quality/quality_typescript_target.go b/internal/codeguard/checks/quality/quality_typescript_target.go index a8b3c66..f62ad30 100644 --- a/internal/codeguard/checks/quality/quality_typescript_target.go +++ b/internal/codeguard/checks/quality/quality_typescript_target.go @@ -14,7 +14,7 @@ var qualityTypeScriptTargetExtract = func(results support.TypeScriptSemanticResu func typeScriptTargetFindings(ctx context.Context, env support.Context, target core.TargetConfig) []core.Finding { results, ok, err := support.AnalyzeTypeScriptTargetForContext(ctx, env, target) if err == nil && ok { - findings := support.FindingsFromInputs(env, qualityTypeScriptTargetExtract(results)) + findings := support.FindingsFromInputs(env, filterTypeScriptQualitySemanticInputs(qualityTypeScriptTargetExtract(results))) findings = append(findings, env.ScanTargetFiles(target, "quality-typescript-file-length", isTypeScriptLikeFile, func(file string, data []byte) []core.Finding { return fileLengthFindingWithSignals(env, file, data, findings) })...) @@ -40,3 +40,18 @@ func typeScriptTargetFindings(ctx context.Context, env support.Context, target c }, }) } + +func filterTypeScriptQualitySemanticInputs(inputs []support.FindingInput) []support.FindingInput { + out := inputs[:0] + for _, input := range inputs { + if isScriptTestOrHelperFile(input.Path) && + (input.RuleID == "quality.typescript.non-null-assertion" || + input.RuleID == "quality.javascript.non-null-assertion" || + input.RuleID == "quality.typescript.double-assertion" || + input.RuleID == "quality.javascript.double-assertion") { + continue + } + out = append(out, input) + } + return out +} diff --git a/internal/codeguard/checks/quality/quality_typescript_tree.go b/internal/codeguard/checks/quality/quality_typescript_tree.go index bf8a96c..285e602 100644 --- a/internal/codeguard/checks/quality/quality_typescript_tree.go +++ b/internal/codeguard/checks/quality/quality_typescript_tree.go @@ -85,6 +85,9 @@ func typeScriptExplicitAnyFindings(ctx typeScriptScanContext, tree *support.Synt } func typeScriptDoubleAssertionFindings(ctx typeScriptScanContext, tree *support.SyntaxTree) []core.Finding { + if isScriptTestOrHelperFile(ctx.file) { + return nil + } regexSpec := support.ScriptRegexSpec{ Pattern: tsDoubleAssertPattern, RuleID: qualityRuleID(ctx.file, "double-assertion"), @@ -116,6 +119,9 @@ func typeScriptDoubleAssertionFindings(ctx typeScriptScanContext, tree *support. func typeScriptNonNullAssertionFindings(ctx typeScriptScanContext, tree *support.SyntaxTree) []core.Finding { ruleID := qualityRuleID(ctx.file, "non-null-assertion") + if isScriptTestOrHelperFile(ctx.file) { + return nil + } if typeScriptTreeUsable(tree) { findings, ok := support.ScriptQueryFindings(ctx.env, ctx.file, tree, support.ScriptQuerySpec{ Query: tsNonNullQuery, diff --git a/tests/checks/quality_environment_branching_test.go b/tests/checks/quality_environment_branching_test.go index 5be00de..d3c4380 100644 --- a/tests/checks/quality_environment_branching_test.go +++ b/tests/checks/quality_environment_branching_test.go @@ -3,6 +3,7 @@ package checks_test import ( "context" "path/filepath" + "strings" "testing" "github.com/devr-tools/codeguard/pkg/codeguard" @@ -34,6 +35,31 @@ func TestQualityEnvironmentBranchingAllowsBootstrapConfig(t *testing.T) { } } +func TestQualityEnvironmentBranchingSkipsStringParsingAndRegexValidation(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "src", "lib", "conversation-ref.ts"), strings.Join([]string{ + "const CONVERSATION_ID_RE = /^production-[a-z0-9]+$/;", + "export function parseConversationRef(value?: string | null) {", + " const trimmed = (value ?? '').trim();", + " if (!trimmed) return null;", + " if (CONVERSATION_ID_RE.test(trimmed)) return trimmed;", + " return findConversationRefInText(trimmed);", + "}", + "declare function findConversationRefInText(value: string): string | null;", + }, "\n")) + + cfg := qualityEnvironmentTestConfig(dir, "environment-branching-string-parsing") + cfg.Targets[0].Language = "typescript" + report, err := codeguard.Run(context.Background(), cfg) + if err != nil { + t.Fatalf("run: %v", err) + } + + if messages := qualityRuleMessages(report, "quality.environment-branching"); len(messages) != 0 { + t.Fatalf("unexpected environment branching findings: %v", messages) + } +} + func qualityEnvironmentTestConfig(dir string, name string) codeguard.Config { cfg := codeguard.ExampleConfig() cfg.Name = name diff --git a/tests/checks/quality_ui_false_positive_hardening_test.go b/tests/checks/quality_ui_false_positive_hardening_test.go index 494da06..a40ae47 100644 --- a/tests/checks/quality_ui_false_positive_hardening_test.go +++ b/tests/checks/quality_ui_false_positive_hardening_test.go @@ -432,6 +432,103 @@ func TestNamingCardinalityMismatchAllowsCommonPluralDomainCollections(t *testing assertFindingRuleAbsent(t, report, "Code Quality", "naming.cardinality-mismatch") } +func TestLMPFalsePositiveHardeningForBoundaryContractsAndNames(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "src", "app", "api", "apps", "lmp", "search.ts"), strings.Join([]string{ + "export async function executeSearch(rawBody: unknown): Promise {", + " const input = parseSearchInput(rawBody);", + " if (!input.ok) return { ok: false, error: input.error };", + " const rows = await repo.search(input.value.query);", + " return { ok: true, value: rows };", + "}", + "interface SearchResponse { ok: boolean; value?: unknown; error?: string }", + "declare const repo: { search(query: string): Promise };", + "declare function parseSearchInput(raw: unknown): { ok: true; value: { query: string } } | { ok: false; error: string };", + }, "\n")) + writeFile(t, filepath.Join(dir, "src", "lib", "lmp", "appearance", "theme.ts"), strings.Join([]string{ + "export function mapAdvancedThemeToGraphql(payload: Record): Record {", + " return { profile: String(payload.profile ?? '') };", + "}", + }, "\n")) + writeFile(t, filepath.Join(dir, "src", "app", "(app)", "apps", "lmp", "_components", "billing", "rows.tsx"), strings.Join([]string{ + "export function asCurrency(amountMinor: number | null | undefined, currency: string | null) {", + " return `${currency ?? 'USD'} ${amountMinor ?? 0}`;", + "}", + }, "\n")) + writeFile(t, filepath.Join(dir, "src", "lib", "lmp", "appearance", "prefill.ts"), strings.Join([]string{ + "export function pickProfile(account: Record, profile: Record) {", + " return { account, profile };", + "}", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + for _, ruleID := range []string{ + "defensive.unvalidated-boundary-input", + "function.mixed-abstraction-level", + "function.orchestration-domain-mix", + "function.partial-result", + "function.inconsistent-return-contract", + "naming.implementation-leak", + "naming.missing-unit", + "naming.cardinality-mismatch", + } { + assertFindingRuleAbsent(t, report, "Code Quality", ruleID) + } +} + +func TestTypeScriptAssertionRulesSkipTestsAndHelpers(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "src", "__tests__", "lib", "lmp", "coach", "context.test.ts"), strings.Join([]string{ + "interface MeshAccount { id: string }", + "const ACCOUNT = { id: 'acct' } as unknown as MeshAccount;", + "export function latest(search: { mock: { calls: unknown[][] } }) {", + " return search.mock.calls[0]![1] as unknown as { body: string };", + "}", + }, "\n")) + writeFile(t, filepath.Join(dir, "src", "lib", "lmp", "coach", "help-centre-sync-test-helpers.ts"), strings.Join([]string{ + "export function mockClient(client: unknown) {", + " return client as unknown as { from(table: string): unknown };", + "}", + }, "\n")) + writeFile(t, filepath.Join(dir, "src", "lib", "lmp", "production.ts"), strings.Join([]string{ + "export function unsafe(input?: { value?: string }) {", + " return input!.value as unknown as string;", + "}", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertCodeQualityRuleAbsentForPath(t, report, "quality.typescript.non-null-assertion", "context.test.ts") + assertCodeQualityRuleAbsentForPath(t, report, "quality.typescript.double-assertion", "context.test.ts") + assertCodeQualityRuleAbsentForPath(t, report, "defensive.unchecked-type-assertion", "context.test.ts") + assertCodeQualityRuleAbsentForPath(t, report, "quality.typescript.double-assertion", "help-centre-sync-test-helpers.ts") + assertCodeQualityRulePresentForPathWithMessage(t, report, "quality.typescript.non-null-assertion", "production.ts", "non-null") + assertCodeQualityRulePresentForPathWithMessage(t, report, "quality.typescript.double-assertion", "production.ts", "double") +} + +func TestDefensiveRulesSkipDTOsGenericParsersAndUIDefaults(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "src", "lib", "lmp", "account-context.ts"), strings.Join([]string{ + "interface MeshAccount { username: string; tier: string }", + "export interface AccountContextBase { username: MeshAccount['username']; tier: MeshAccount['tier']; includeInactive?: boolean }", + "interface DevinDispatchResult { ok: boolean; pending?: boolean }", + "interface LinearIssueStatesResponse { issues: { nodes: { id: string; state: { type: string } }[] } }", + "export function safeJsonParse(raw: string): { ok: true; value: unknown } | { ok: false } {", + " try { return { ok: true, value: JSON.parse(raw) }; } catch { return { ok: false }; }", + "}", + "export function toolbarConfig(config: { enabled?: boolean; docked?: boolean; minimized?: boolean }) {", + " return { enabled: config.enabled === true, docked: config.docked !== false, minimized: config.minimized === true };", + "}", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.invalid-state-representable") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-schema-validation") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unsafe-default") +} + func TestQualityMutableGlobalStateIgnoresReactLocalBindings(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "apps/web/app/claims/claim-classification-fields.tsx"), strings.Join([]string{