diff --git a/docs/checks.md b/docs/checks.md index a84be69..2de9ffb 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -1186,6 +1186,7 @@ These rules live outside the repository-wide `Change Safety` section in report o | Defensive programming | `defensive.invalid-state-representable` | warn | Booleans or raw status strings can represent impossible state combinations. | | Defensive programming | `defensive.null-assumption` | warn | Nullable boundary values are dereferenced without a nil/null guard. | | Defensive programming | `defensive.integer-overflow` | warn | Arithmetic on count, size, or length input lacks an overflow bound check. | +| Defensive programming | `defensive.sequence-collision-risk` | warn | External ID allocation derives the next value from current count without guarded unique-collision retry. | | Defensive programming | `defensive.bounds-assumption` | warn | Indexed access assumes collection bounds without a nearby length check. | | Defensive programming | `defensive.unsafe-default` | warn | A config/env fallback can fail open or disable a safety control. | | Defensive programming | `defensive.non-exhaustive-branch` | warn | Enum-like state/kind/type branching lacks default or exhaustive handling. | diff --git a/internal/codeguard/checks/quality/quality_defensive.go b/internal/codeguard/checks/quality/quality_defensive.go index 79946ca..2e93d04 100644 --- a/internal/codeguard/checks/quality/quality_defensive.go +++ b/internal/codeguard/checks/quality/quality_defensive.go @@ -13,6 +13,7 @@ const ( defensiveInvalidStateRepresentableRuleID = "defensive.invalid-state-representable" defensiveNullAssumptionRuleID = "defensive.null-assumption" defensiveIntegerOverflowRuleID = "defensive.integer-overflow" + defensiveSequenceCollisionRiskRuleID = "defensive.sequence-collision-risk" defensiveBoundsAssumptionRuleID = "defensive.bounds-assumption" defensiveUnsafeDefaultRuleID = "defensive.unsafe-default" defensiveNonExhaustiveBranchRuleID = "defensive.non-exhaustive-branch" @@ -35,6 +36,10 @@ var ( structStartPattern = regexp.MustCompile(`(?i)\b(type\s+\w+\s+struct|interface\s+\w+|class\s+\w+|struct\s+\w+)`) boolFieldPattern = regexp.MustCompile(`(?i)\b(bool|boolean)\b`) stringStateFieldPattern = regexp.MustCompile(`(?i)\b(status|state|kind)\b.*\b(string|str|std::string|String)\b|\b(string|str|std::string|String)\b.*\b(status|state|kind)\b`) + resourceCountGuard = regexp.MustCompile(`(?i)\b(?:count|size|length|len|bytes)\s*(?:<=|<|>|>=)\s*(?:max|limit|quota|cap|[0-9])`) + resourceNamedCountLimit = regexp.MustCompile(`(?i)\b(?:max|limit|quota|cap)[A-Za-z0-9_]*(?:count|size|length|len|bytes)\b`) + sequenceAllocationLine = regexp.MustCompile(`(?i)\b(?:external[_]?id|next[_]?id|sequence|slug|number)\b.*(?:count|max)\s*\+\s*1|(?:count|max)\s*\+\s*1.*\b(?:external[_]?id|next[_]?id|sequence|slug|number)\b`) + jsonReaderSchemaCall = regexp.MustCompile(`(?i)\b(?:read|parse|decode)Json[A-Za-z0-9_]*\s*\([^)\n,]+,\s*[A-Za-z_$][\w$]*(?:Schema|Validator|Codec|Parser)\b`) ) func defensiveBoundaryFindings(env support.Context, file string, fn precisionFunction) []core.Finding { @@ -53,6 +58,10 @@ func defensiveBoundaryFindings(env support.Context, file string, fn precisionFun findings = append(findings, precisionWarnFinding(env, defensiveNullAssumptionRuleID, file, line, "nullable boundary value is dereferenced without a nil/null guard", core.ConfidenceMedium)) } + if line, ok := sequenceCollisionRiskLine(fn, loweredBody); ok { + findings = append(findings, precisionWarnFinding(env, defensiveSequenceCollisionRiskRuleID, file, line, + "external ID allocation derives the next value from current count without guarded unique-collision retry", core.ConfidenceMedium)) + } if line, ok := integerOverflowLine(file, fn, loweredBody); ok { findings = append(findings, precisionWarnFinding(env, defensiveIntegerOverflowRuleID, file, line, "arithmetic on count, size, or length input lacks an overflow bound check", core.ConfidenceMedium)) @@ -130,6 +139,9 @@ func unvalidatedBoundaryInputLine(fn precisionFunction, loweredBody string) (int if !boundaryFunctionName(fn.Name) && !hasBoundaryParam(fn.Params) { return 0, false } + if isValidationOrExtractionHelperName(fn.Name) { + return 0, false + } if validatedBoundaryInputPattern(fn, loweredBody) { return 0, false } @@ -149,7 +161,13 @@ func formDataHasContentLengthPreflight(loweredBody string) bool { } func validatedBoundaryInputPattern(fn precisionFunction, loweredBody string) bool { - if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "zod.", "yup.", "pydantic", "jsonschema"}) { + if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "z.safeparse", "zod.", "yup.", "pydantic", "jsonschema"}) { + return true + } + if jsonReaderSchemaCall.MatchString(functionRawBody(fn)) { + return true + } + if strings.Contains(loweredBody, "nextresponse.") && containsAny(loweredBody, []string{"return nextresponse", ".json(", "redirect("}) && containsAny(loweredBody, []string{"if (!", "if (!", "if(", "if "}) { return true } if regexp.MustCompile(`(?i)\b(parse|assert|guard|ensure|decode)[A-Z_][A-Za-z0-9_]*(?:Input|Payload|Body|Params|Query|Record|Request|Event|Config)?\s*\(`).MatchString(functionRawBody(fn)) { @@ -158,6 +176,16 @@ func validatedBoundaryInputPattern(fn precisionFunction, loweredBody string) boo return false } +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") { + return true + } + return containsAny(lowered, []string{"bearertokenfrom", "tokenfrom", "headerfrom", "requestbodyfrom"}) +} + func hasBoundaryParam(params []support.ParsedParam) bool { for _, param := range params { name := strings.ToLower(param.Name) @@ -205,7 +233,7 @@ func integerOverflowLine(file string, fn precisionFunction, loweredBody string) if isUIRenderArithmeticContext(file, fn, loweredBody) { return 0, false } - if guardedSequenceCollisionRetry(loweredBody) { + if sequenceAllocationArithmetic(loweredBody) || metricStatArithmeticContext(fn, loweredBody) || dateCountFormattingContext(fn, loweredBody) { return 0, false } if containsAny(loweredBody, []string{"maxint", "math.max", "checked", "saturating", "overflow", "limits<", "safeint"}) { @@ -220,7 +248,35 @@ func integerOverflowLine(file string, fn precisionFunction, loweredBody string) return 0, false } +func sequenceCollisionRiskLine(fn precisionFunction, loweredBody string) (int, bool) { + if !sequenceAllocationArithmetic(loweredBody) || guardedSequenceCollisionRetry(loweredBody) { + return 0, false + } + return firstSequenceAllocationLine(fn), true +} + +func sequenceAllocationArithmetic(loweredBody string) bool { + if !containsAny(loweredBody, []string{"count + 1", "count+1", "max + 1", "max+1"}) { + return false + } + return containsAny(loweredBody, []string{"externalid", "external_id", "nextid", "next_id", "sequence", "slug", "number"}) +} + +func firstSequenceAllocationLine(fn precisionFunction) int { + for _, statement := range fn.Statements { + raw := firstNonEmptyString(statement.Raw, statement.Text) + if sequenceAllocationLine.MatchString(raw) { + return statement.Line + } + } + return fn.StartLine +} + func guardedSequenceCollisionRetry(loweredBody string) bool { + if containsAny(loweredBody, []string{"withexternalidretry", "with_external_id_retry"}) && + containsAny(loweredBody, []string{"p2002", "unique", "collision", "externalid", "external_id"}) { + return true + } if !containsAny(loweredBody, []string{"p2002", "unique", "collision", "prisma"}) { return false } @@ -230,6 +286,25 @@ func guardedSequenceCollisionRetry(loweredBody string) bool { return containsAny(loweredBody, []string{"count + 1", "count+1", "externalid", "external_id", "nextid", "next_id"}) } +func metricStatArithmeticContext(fn precisionFunction, loweredBody string) bool { + loweredName := strings.ToLower(fn.Name) + if containsAny(loweredName, []string{"metric", "metrics", "stat", "stats", "counter", "histogram", "telemetry"}) { + return true + } + return containsAny(loweredBody, []string{"metric.", "metrics.", "counter.", "histogram", "stat.", "stats.", "telemetry", "prometheus", "datadog"}) +} + +func dateCountFormattingContext(fn precisionFunction, loweredBody string) bool { + loweredName := strings.ToLower(fn.Name) + if !containsAny(loweredName, []string{"format", "display", "label", "render", "summary", "calendar", "date", "time"}) { + return false + } + return containsAny(loweredBody, []string{ + "date", "time", "calendar", "duration", "intl.", "datetimeformat", "formatdistance", + "formatrelative", "plural", "label", "title", "subtitle", "`${", " + \"", " + '", + }) +} + func isUIRenderArithmeticContext(file string, fn precisionFunction, loweredBody string) bool { if isUIHelperOrMappingContext(file, fn) { return true @@ -333,7 +408,7 @@ func missingSchemaValidationLine(fn precisionFunction, loweredBody string) (int, if !jsonDecodePattern.MatchString(functionRawBody(fn)) { return 0, false } - if validatedBoundaryInputPattern(fn, loweredBody) || containsAny(loweredBody, []string{"jsonschema", "isvalid", "required"}) { + if validatedBoundaryInputPattern(fn, loweredBody) || jsonReaderSchemaCall.MatchString(functionRawBody(fn)) || containsAny(loweredBody, []string{"jsonschema", "isvalid", "required"}) { return 0, false } return firstPatternLine(fn, jsonDecodePattern), true @@ -343,7 +418,10 @@ func missingResourceLimitLine(fn precisionFunction, loweredBody string) (int, bo if !resourceReadPattern.MatchString(functionRawBody(fn)) { return 0, false } - if containsAny(loweredBody, []string{"limitreader", "maxbytes", "max_bytes", "content-length", "contentlength", "limit(", "take(", "buffer_size", "quota"}) { + if uploadValidationHelperPattern(loweredBody) { + return 0, false + } + if resourceLimitProofPattern(loweredBody) { return 0, false } if boundedReadByteLengthCheck(loweredBody) { @@ -352,6 +430,21 @@ func missingResourceLimitLine(fn precisionFunction, loweredBody string) (int, bo return firstPatternLine(fn, resourceReadPattern), true } +func resourceLimitProofPattern(loweredBody string) bool { + if containsAny(loweredBody, []string{ + "limitreader", "maxbytes", "max_bytes", "content-length", "contentlength", + "limit(", "take(", "slice(", ".slice(", "buffer_size", "quota", + }) { + return true + } + return resourceCountGuard.MatchString(loweredBody) || resourceNamedCountLimit.MatchString(loweredBody) +} + +func uploadValidationHelperPattern(loweredBody string) bool { + return containsAny(loweredBody, []string{"validateinternaluploadfile", "validateuploadfile", "validatefileupload", "validateupload"}) || + containsAny(loweredBody, []string{"internal_upload_max_bytes", "upload_max_bytes", "max_upload_bytes", "max_file_bytes"}) +} + func boundedReadByteLengthCheck(loweredBody string) bool { if !containsAny(loweredBody, []string{"arraybuffer", ".text", "readall", ".read"}) { return false diff --git a/internal/codeguard/checks/quality/quality_precision_mutation_targets.go b/internal/codeguard/checks/quality/quality_precision_mutation_targets.go index 8248083..8b3dddf 100644 --- a/internal/codeguard/checks/quality/quality_precision_mutation_targets.go +++ b/internal/codeguard/checks/quality/quality_precision_mutation_targets.go @@ -10,7 +10,7 @@ import ( 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*\(|^array\.from\b|\.map\s*\(|\.filter\s*\(|\.reduce\s*\(|^(?:bytes|strings)\.buffer\b|^strings\.builder\b`) +var localAccumulatorExprPattern = regexp.MustCompile(`(?i)^(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b|^\[|^\{|^make\s*\(|^array\.from\b|\.map\s*\(|\.filter\s*\(|\.reduce\s*\(|\.split\s*\(|cheerio\.load\s*\(|^(?:bytes|strings)\.buffer\b|^strings\.builder\b`) func localMutationTargets(fn precisionFunction) map[string]struct{} { params := paramNames(fn) @@ -133,7 +133,7 @@ func isAccumulatorLikeLocalName(name string) bool { "bucket", "buckets", "buffer", "builder", "calendar", "cells", "copy", "doc", "document", "filter", "filters", "form", "items", "lines", "params", "parts", "payload", "primarycells", "query", "result", "rows", "scopes", "sections", - "serializer", "text", "urlparams", "values", "csv", "export", "map", + "serializer", "text", "urlparams", "values", "csv", "export", "map", "$", } { if strings.Contains(lowered, token) { return true @@ -146,7 +146,7 @@ func isAccumulatorBuilderFunctionName(name string) bool { lowered := strings.ToLower(strings.Trim(name, "_$")) for _, token := range []string{ "bucket", "build", "collect", "derive", "format", "group", "map", "parse", - "primary", "render", "serialize", "transform", + "primary", "render", "serialize", "transform", "clean", "filter", } { if strings.Contains(lowered, token) { return true @@ -178,6 +178,9 @@ func paramNames(fn precisionFunction) map[string]struct{} { } func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool { + if isDerivedCollectionMutationCall(callee) { + return true + } if isBareLocalMutationCall(callee) { return true } @@ -188,6 +191,11 @@ func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool { return isLocalMutationTarget(target, localTargets) } +func isDerivedCollectionMutationCall(callee string) bool { + lowered := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(callee), " ", "")) + return strings.Contains(lowered, ".split.") && (strings.HasSuffix(lowered, ".pop") || strings.HasSuffix(lowered, ".sort") || strings.HasSuffix(lowered, ".reverse")) +} + func isBareLocalMutationCall(callee string) bool { switch strings.TrimSpace(callee) { case "append", "Set", "Array", "Object", "Map", "WeakMap", "WeakSet", "push_back": @@ -202,6 +210,9 @@ func mutationCallTarget(callee string) string { if callee == "" { return "" } + if strings.HasPrefix(callee, "$.") { + return "$" + } for _, sep := range []string{".", "->", "::"} { if idx := strings.Index(callee, sep); idx > 0 { return strings.TrimSpace(callee[:idx]) diff --git a/internal/codeguard/checks/quality/quality_precision_retune_helpers.go b/internal/codeguard/checks/quality/quality_precision_retune_helpers.go new file mode 100644 index 0000000..c2f3747 --- /dev/null +++ b/internal/codeguard/checks/quality/quality_precision_retune_helpers.go @@ -0,0 +1,49 @@ +package quality + +import "strings" + +func isDomainSideEffectBoundaryName(name string) bool { + lowered := strings.ToLower(strings.TrimSpace(name)) + if lowered == "" { + return false + } + if strings.HasPrefix(lowered, "maybe") && containsAny(lowered, []string{"alert", "notify", "record", "track", "emit"}) { + return true + } + if strings.HasPrefix(lowered, "evaluate") && containsAny(lowered, []string{"abuse", "policy", "rule", "risk", "fraud", "quota", "limit"}) { + return true + } + if strings.HasPrefix(lowered, "load") && containsAny(lowered, []string{"config", "defaults", "settings", "policy"}) { + return true + } + return false +} + +func isAdapterOrOrchestrationFunction(file string, fn precisionFunction) bool { + loweredName := strings.ToLower(strings.Trim(fn.Name, "_$")) + if containsAny(loweredName, []string{"adapter", "bugreport", "bug_report", "slack", "webhook", "sync", "abuseconfig", "abuse_config"}) { + 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"}) { + return true + } + } + normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) + return containsAny(normalized, []string{"/adapters/", "/adapter/", "/connectors/", "/connector/", "/integrations/", "/webhooks/", "/slack/", "/jobs/"}) +} + +func isAdapterOrchestrationName(name string) bool { + loweredName := strings.ToLower(strings.Trim(name, "_$")) + return containsAny(loweredName, []string{"abuseconfig", "abuse_config", "bugreport", "bug_report", "slack", "webhook", "adapter"}) +} + +func configuredPluralDomainAbbreviation(name string) bool { + switch name { + case "docs", "krs": + return true + default: + return false + } +} diff --git a/internal/codeguard/checks/quality/quality_precision_ui_conventions.go b/internal/codeguard/checks/quality/quality_precision_ui_conventions.go index f8e4943..af8e691 100644 --- a/internal/codeguard/checks/quality/quality_precision_ui_conventions.go +++ b/internal/codeguard/checks/quality/quality_precision_ui_conventions.go @@ -161,7 +161,13 @@ func isAllowedBooleanUIName(file string, fn precisionFunction, name string) bool if !isReactComponentOrHookBoundary(file, fn) { return false } - switch strings.ToLower(strings.Trim(name, "_$")) { + normalized := strings.ToLower(strings.Trim(name, "_$")) + for _, suffix := range []string{"active", "visible", "enabled", "disabled", "open", "closed", "expanded", "collapsed", "selected", "checked", "pending", "loading"} { + if strings.HasSuffix(normalized, suffix) { + return true + } + } + switch normalized { case "open", "loading", "active", "pending", "checked", "selected", "expanded", "collapsed": return true default: @@ -191,7 +197,7 @@ func isResourceIdentifierName(name string) bool { func conventionalCardinalityName(name string) bool { base := strings.ToLower(strings.Trim(name, "_$")) switch base { - case "answers", "args", "claims", "columns", "contracts", "entries", "files", "ids", "items", "k", "keys", "matters", "messages", "next", "out", "params", "props", "records", "risks", "rows", "searchparams", "sections", "source", "status", "thresholds", "users", "versions", "v", "i", "j", "x", "y": + case "all", "answers", "args", "claims", "columns", "contracts", "docs", "entries", "files", "filtered", "ids", "items", "k", "keys", "krs", "matters", "messages", "next", "out", "params", "props", "records", "risks", "rows", "searchparams", "sections", "source", "status", "thresholds", "users", "versions", "v", "i", "j", "x", "y": return true default: return len(name) <= 2 || diff --git a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go index 95666b6..170987e 100644 --- a/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go +++ b/internal/codeguard/checks/quality/quality_precision_workstreams_cd.go @@ -112,6 +112,9 @@ func precisionNamingFindings(env support.Context, file string, fn precisionFunct if item.name == "" { continue } + if item.name == fn.Name && isReactComponentOrHookBoundary(file, fn) { + continue + } 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)) @@ -326,39 +329,10 @@ func explicitMutationName(name string) bool { strings.Contains(lowered, "write") } -func isDomainSideEffectBoundaryName(name string) bool { - lowered := strings.ToLower(strings.TrimSpace(name)) - if lowered == "" { +func inconsistentReturnContract(fn precisionFunction) bool { + if nextResponseNullableGuardHelper(fn) { return false } - if strings.HasPrefix(lowered, "maybe") && containsAny(lowered, []string{"alert", "notify", "record", "track", "emit"}) { - return true - } - if strings.HasPrefix(lowered, "evaluate") && containsAny(lowered, []string{"abuse", "policy", "rule", "risk", "fraud", "quota", "limit"}) { - return true - } - if strings.HasPrefix(lowered, "load") && containsAny(lowered, []string{"config", "defaults", "settings", "policy"}) { - return true - } - return false -} - -func isAdapterOrOrchestrationFunction(file string, fn precisionFunction) bool { - loweredName := strings.ToLower(strings.Trim(fn.Name, "_$")) - if containsAny(loweredName, []string{"adapter", "bugreport", "bug_report", "slack", "webhook", "sync", "abuseconfig", "abuse_config"}) { - 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"}) { - return true - } - } - normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) - return containsAny(normalized, []string{"/adapters/", "/adapter/", "/connectors/", "/connector/", "/integrations/", "/webhooks/", "/slack/", "/jobs/"}) -} - -func inconsistentReturnContract(fn precisionFunction) bool { returns := returnCategories(fn.Body) if returns.total < 2 { return false @@ -366,6 +340,17 @@ func inconsistentReturnContract(fn precisionFunction) bool { return returns.empty && returns.value } +func nextResponseNullableGuardHelper(fn precisionFunction) bool { + signature := strings.ToLower(fn.Signature) + body := strings.ToLower(fn.Body) + hasNullableSignature := strings.Contains(signature, "nextresponse") && strings.Contains(signature, "null") + hasNextResponseBody := strings.Contains(body, "nextresponse.") || strings.Contains(body, "return new nextresponse") + if !hasNullableSignature && !hasNextResponseBody { + return false + } + return strings.Contains(body, "return null") && hasNextResponseBody +} + type returnShapeCounts struct { total int empty bool @@ -443,11 +428,6 @@ func responsibilityCount(fn precisionFunction) (int, []string) { return len(labels), labels } -func isAdapterOrchestrationName(name string) bool { - loweredName := strings.ToLower(strings.Trim(name, "_$")) - return containsAny(loweredName, []string{"abuseconfig", "abuse_config", "bugreport", "bug_report", "slack", "webhook", "adapter"}) -} - func classifyResponsibility(text string, record func(string)) { switch { case strings.Contains(text, "validat") || strings.Contains(text, "sanitize"): @@ -523,7 +503,7 @@ func isBooleanType(typ string) bool { func isPredicateName(name string) bool { lowered := strings.ToLower(strings.Trim(name, "_$")) - for _, prefix := range []string{"is", "are", "has", "have", "can", "could", "should", "must", "allow", "allows", "enable", "enabled", "disable", "disabled", "needs", "requires", "supports", "valid", "visible", "ready"} { + for _, prefix := range []string{"is", "are", "has", "have", "can", "could", "should", "must", "allow", "allows", "enable", "enabled", "disable", "disabled", "needs", "requires", "supports", "valid", "visible", "ready", "show", "matches"} { if strings.HasPrefix(lowered, prefix) { return true } @@ -533,7 +513,7 @@ func isPredicateName(name 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") { + if base == "" || conventionalCardinalityName(base) || configuredPluralDomainAbbreviation(base) || strings.HasSuffix(base, "status") || strings.HasSuffix(base, "class") { return false } plural := isPluralName(base) diff --git a/internal/codeguard/checks/quality/quality_smells.go b/internal/codeguard/checks/quality/quality_smells.go index 0a945c4..75102e6 100644 --- a/internal/codeguard/checks/quality/quality_smells.go +++ b/internal/codeguard/checks/quality/quality_smells.go @@ -603,6 +603,9 @@ func normalizedParamConcept(name string) string { func switchOnTypeFindings(env support.Context, file string, source string, language string) []core.Finding { masked := maskForStructuralLanguage(source, language) + if centralizedEnumDispatchContext(file, masked) { + return nil + } typeBranches := len(typeBranchPattern.FindAllStringIndex(masked, -1)) kindBranches := 0 switch language { @@ -625,6 +628,19 @@ func switchOnTypeFindings(env support.Context, file string, source string, langu return nil } +func centralizedEnumDispatchContext(file string, source string) bool { + normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/")) + lowered := strings.ToLower(source) + if containsAny(normalized, []string{"label", "labels", "status", "statuses", "option", "options", "map", "maps", "display"}) { + return true + } + if containsAny(lowered, []string{"label:", "labels", "record<", " as const", "satisfies record", "displayname", "display_name"}) && + !regexp.MustCompile(`(?is)\b(?:if|switch)\b.*\b(?:save|update|delete|send|publish|emit|write)\b`).MatchString(lowered) { + return true + } + return false +} + func firstTypeBranchLine(masked string) int { idx := len(masked) for _, locs := range [][]int{ diff --git a/internal/codeguard/rules/catalog_fix_templates_quality_errors_defensive.go b/internal/codeguard/rules/catalog_fix_templates_quality_errors_defensive.go index 05000ae..0014fff 100644 --- a/internal/codeguard/rules/catalog_fix_templates_quality_errors_defensive.go +++ b/internal/codeguard/rules/catalog_fix_templates_quality_errors_defensive.go @@ -18,6 +18,7 @@ var qualityErrorDefensiveFixTemplates = map[string]core.FixTemplate{ "defensive.invalid-state-representable": {Kind: guided, Text: "Replace boolean combinations/raw strings with an enum, tagged union, or state machine that encodes valid states."}, "defensive.null-assumption": {Kind: guided, Text: "Guard nil/null/None/optional values before dereference, or make the boundary type non-nullable."}, "defensive.integer-overflow": {Kind: guided, Text: "Guard count/size arithmetic before multiplication, addition, shifts, or allocation sizing."}, + "defensive.sequence-collision-risk": {Kind: guided, Text: "Replace count-plus-one external IDs with database sequences/UUIDs, or wrap allocation in a bounded unique-collision retry."}, "defensive.bounds-assumption": {Kind: guided, Text: "Check length/existence before indexing, or use a safe lookup API."}, "defensive.unsafe-default": {Kind: guided, Text: "Make security/safety defaults fail closed and require explicit opt-out for unsafe behavior."}, "defensive.non-exhaustive-branch": {Kind: guided, Text: "Add an explicit default/unreachable branch or exhaustive assertion for enum-like state switches."}, diff --git a/internal/codeguard/rules/catalog_quality_errors_defensive.go b/internal/codeguard/rules/catalog_quality_errors_defensive.go index 78f1272..72618f9 100644 --- a/internal/codeguard/rules/catalog_quality_errors_defensive.go +++ b/internal/codeguard/rules/catalog_quality_errors_defensive.go @@ -19,6 +19,7 @@ var qualityErrorDefensiveCatalog = map[string]core.RuleMetadata{ "defensive.invalid-state-representable": localQualityRule("defensive.invalid-state-representable", "warn", "Invalid state representable", "Warns when booleans or raw status strings can represent impossible state combinations.", "Model state with an enum, sum type, or value object that makes invalid combinations unrepresentable."), "defensive.null-assumption": localQualityRule("defensive.null-assumption", "warn", "Null assumption", "Warns when nullable boundary values are dereferenced without a nil/null guard.", "Check for nil/null or validate the value before dereferencing."), "defensive.integer-overflow": localQualityRule("defensive.integer-overflow", "warn", "Integer overflow assumption", "Warns when arithmetic on count, size, or length input lacks an overflow bound check.", "Validate bounds or use a type wide enough for the source range before arithmetic."), + "defensive.sequence-collision-risk": localQualityRule("defensive.sequence-collision-risk", "warn", "Sequence collision risk", "Warns when external ID allocation derives the next value from current count without guarded unique-collision retry.", "Use a database sequence/UUID or handle unique-collision retries explicitly."), "defensive.bounds-assumption": localQualityRule("defensive.bounds-assumption", "warn", "Bounds assumption", "Warns when indexed access assumes collection bounds without a nearby length check.", "Check length or key existence before indexing."), "defensive.unsafe-default": localQualityRule("defensive.unsafe-default", "warn", "Unsafe default", "Warns when a config/env fallback can fail open or disable a safety control.", "Choose fail-closed defaults and require explicit opt-out for safety-sensitive settings."), "defensive.non-exhaustive-branch": localQualityRule("defensive.non-exhaustive-branch", "warn", "Non-exhaustive branch", "Warns when enum-like state/kind/type branching lacks default or exhaustive handling.", "Handle every known case and add a safe default or exhaustive assertion."), diff --git a/tests/checks/function_hidden_mutation_noise_test.go b/tests/checks/function_hidden_mutation_noise_test.go index 7478701..7676226 100644 --- a/tests/checks/function_hidden_mutation_noise_test.go +++ b/tests/checks/function_hidden_mutation_noise_test.go @@ -170,6 +170,63 @@ func TestFunctionHiddenMutationAllowsBuilderParserAccumulatorNames(t *testing.T) } } +func TestFunctionHiddenMutationAllowsMoreLocalBuilderMutationIdioms(t *testing.T) { + cases := []struct { + name string + file string + source []string + }{ + { + name: "contracts filters set and sort", + file: "apps/web/app/contracts/contracts-filters.ts", + source: []string{ + "export function buildContractFilters(rows: Contract[]) {", + " const statuses = new Set();", + " const defs: Array<{ value: string }> = [];", + " for (const row of rows) statuses.add(row.status);", + " for (const status of statuses) defs.push({ value: status });", + " defs.sort((a, b) => a.value.localeCompare(b.value));", + " return defs;", + "}", + "interface Contract { status: string }", + }, + }, + { + name: "split pop parser", + file: "packages/api/src/parse.ts", + source: []string{ + "export function parseFileExtension(name: string) {", + " return name.split('.').pop() ?? '';", + "}", + }, + }, + { + name: "cheerio cleanup", + file: "packages/api/src/clean-html.ts", + source: []string{ + "import * as cheerio from 'cheerio';", + "export function cleanHtml(html: string) {", + " const $ = cheerio.load(html);", + " $('script').remove();", + " $('[style]').removeAttr('style');", + " return $.html();", + "}", + }, + }, + } + 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") + assertFindingRuleAbsent(t, report, "Code Quality", "function.command-query-mix") + }) + } +} + func TestFunctionHiddenMutationStillWarnsForCollaboratorMutationWithLocalPayload(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "mutation.ts"), strings.Join([]string{ diff --git a/tests/checks/quality_precision_retune_false_positive_test.go b/tests/checks/quality_precision_retune_false_positive_test.go new file mode 100644 index 0000000..26760ad --- /dev/null +++ b/tests/checks/quality_precision_retune_false_positive_test.go @@ -0,0 +1,360 @@ +package checks_test + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestQualityPrecisionAllowsDomainSideEffectAndAdapterOrchestrationNames(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/connectors/src/abuse.ts"), strings.Join([]string{ + "export async function loadAbuseConfig(repo: Repo) {", + " const config = await repo.load();", + " await repo.update(config);", + " return config;", + "}", + "export async function evaluateActionAbuse(evaluator: Evaluator, action: Action) {", + " const result = await evaluator.evaluate(action);", + " await evaluator.record(result);", + " return result;", + "}", + "export async function maybeAlert(alerts: Alerts, result: Result) {", + " if (result.highRisk) await alerts.send(result);", + " return result;", + "}", + "export async function saveAbuseConfig(repo: Repo, input: Input) {", + " const parsed = parseInput(input);", + " await repo.save(parsed);", + " await repo.audit(parsed);", + " return parsed;", + "}", + "export async function postBugReportToSlack(slack: Slack, report: Report) {", + " const body = formatReport(report);", + " await slack.post(body);", + " await slack.record(body);", + " return body;", + "}", + "interface Repo { load(): Promise; update(input: unknown): Promise; save(input: unknown): Promise; audit(input: unknown): Promise }", + "interface Evaluator { evaluate(input: unknown): Promise; record(input: unknown): Promise }", + "interface Alerts { send(input: unknown): Promise }", + "interface Slack { post(input: unknown): Promise; record(input: unknown): Promise }", + "interface Input { id: string }", + "interface Action { id: string }", + "interface Result { highRisk: boolean }", + "interface Report { id: string }", + "declare function parseInput(input: Input): unknown;", + "declare function formatReport(report: Report): unknown;", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + for _, ruleID := range []string{ + "function.hidden-mutation", + "function.multiple-responsibilities", + "function.mixed-abstraction-level", + "quality.mixed-abstraction-levels", + "smell.feature-envy", + } { + assertFindingRuleAbsent(t, report, "Code Quality", ruleID) + } +} + +func TestQualityNamingAllowsCommonConnectorNames(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/connectors/src/normalizers.ts"), strings.Join([]string{ + "export function asRecord(value: unknown): Record {", + " return typeof value === 'object' && value !== null ? value as Record : {};", + "}", + "export function areStickerPlacementsEqual(source: Placement[], keys: string[], thresholds: Record, cached: boolean) {", + " const value = source.length === keys.length;", + " return cached && value && Object.keys(thresholds).length > 0;", + "}", + "interface Placement { id: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "naming.boolean-not-predicate") + assertFindingRuleAbsent(t, report, "Code Quality", "naming.cardinality-mismatch") + assertFindingRuleAbsent(t, report, "Code Quality", "quality.ambiguous-name") +} + +func TestQualityDuplicatedKnowledgeSkipsTableAndEnumValueLiterals(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/connectors/src/constants.ts"), strings.Join([]string{ + "export const abuseTableName = 'lmp_abuse_config';", + "export const auditTableName = 'lmp_abuse_config';", + "export const volumeOptions = [{ value: 'high_volume' }, { value: 'high_volume' }];", + "export const avatarOptions = [{ value: 'custom_avatar' }, { value: 'custom_avatar' }];", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "quality.duplicated-knowledge") +} + +func TestDefensiveRulesRecognizeValidationAndBoundedReadProofs(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/connectors/src/boundary.ts"), strings.Join([]string{ + "export function handleToolAction(payload: Record) {", + " const parsed = parseToolActionPayload(payload);", + " return parsed.action;", + "}", + "export async function POST(request: Request) {", + " const contentLength = Number(request.headers.get('content-length') ?? '0');", + " if (contentLength > MAX_UPLOAD_BYTES) throw new Error('upload too large');", + " const form = await request.formData();", + " return Response.json({ ok: true, form });", + "}", + "export async function readBounded(response: Response) {", + " const bytes = await response.arrayBuffer();", + " if (bytes.byteLength > MAX_RESPONSE_BYTES) throw new Error('response too large');", + " return bytes;", + "}", + "export function normalizeWebhookUrl(input: string) {", + " const url = new URL(input);", + " if (url.protocol !== 'https:') throw new Error('invalid protocol');", + " return url;", + "}", + "function parseToolActionPayload(value: Record) {", + " const result = ToolActionSchema.safeParse(value);", + " if (!result.success) throw new Error('invalid payload');", + " return result.data;", + "}", + "declare const ToolActionSchema: { safeParse(value: unknown): { success: boolean; data: { action: string } } };", + "declare const MAX_UPLOAD_BYTES: number;", + "declare const MAX_RESPONSE_BYTES: number;", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unvalidated-boundary-input") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-schema-validation") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-resource-limit") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unchecked-external-response") +} + +func TestDefensiveRulesRecognizeUploadHelpersAndRouteValidationGuards(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/files/use-new-version-upload.ts"), strings.Join([]string{ + "import { INTERNAL_UPLOAD_MAX_BYTES, validateInternalUploadFile } from './upload-validation';", + "export async function uploadVersion(file: File) {", + " validateInternalUploadFile(file, INTERNAL_UPLOAD_MAX_BYTES);", + " await uploadClient.upload(file);", + " return file.name;", + "}", + "declare const uploadClient: { upload(file: File): Promise };", + }, "\n")) + writeFile(t, filepath.Join(dir, "apps/web/app/api/files/route.ts"), strings.Join([]string{ + "import { NextResponse } from 'next/server';", + "export async function POST(request: Request) {", + " const parsed = await parseRequestBody(request);", + " if (!parsed.ok) return NextResponse.json({ error: 'invalid' }, { status: 400 });", + " const url = new URL(parsed.value.callbackUrl);", + " if (url.protocol !== 'https:') return NextResponse.json({ error: 'invalid' }, { status: 400 });", + " return NextResponse.json({ ok: true });", + "}", + "export async function parseRequestBody(request: Request) {", + " const body = await request.json();", + " const result = z.safeParse(body);", + " if (!result.success) return { ok: false as const };", + " return { ok: true as const, value: result.data };", + "}", + "export function bearerTokenFrom(request: Request) {", + " const header = request.headers.get('authorization');", + " if (!header?.startsWith('Bearer ')) return null;", + " return header.slice('Bearer '.length);", + "}", + "declare const z: { safeParse(value: unknown): { success: boolean; data: { callbackUrl: string } } };", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-resource-limit") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unvalidated-boundary-input") +} + +func TestQualityNamingAllowsUIBooleanAndDomainCollectionAliases(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/contracts/filters.tsx"), strings.Join([]string{ + "export function ContractFilters(showAdd: boolean, showDeprioritized: boolean, filtersActive: boolean, matchesFilter: boolean, krs: KeyResult[], docs: Document[], filtered: Contract[], all: Contract[]) {", + " return
{String(showAdd && showDeprioritized && filtersActive && matchesFilter)}{krs.length}{docs.length}{filtered.length}{all.length}
;", + "}", + "interface KeyResult { id: string }", + "interface Contract { id: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "naming.boolean-not-predicate") + assertFindingRuleAbsent(t, report, "Code Quality", "naming.cardinality-mismatch") +} + +func TestSwitchOnTypeAllowsCentralizedEnumDisplayMaps(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/contracts/status-labels.ts"), strings.Join([]string{ + "export function labelForContractStatus(status: ContractStatus) {", + " switch (status.type) {", + " case 'draft': return 'Draft';", + " case 'review': return 'In review';", + " case 'signed': return 'Signed';", + " case 'archived': return 'Archived';", + " default: return 'Unknown';", + " }", + "}", + "export const statusOptions = {", + " draft: { label: 'Draft' },", + " review: { label: 'In review' },", + " signed: { label: 'Signed' },", + "} as const;", + "interface ContractStatus { type: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "smell.switch-on-type") +} + +func TestDefensiveIntegerOverflowSkipsGuardedP2002SequenceRetry(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/api/src/external-id.ts"), strings.Join([]string{ + "export async function allocateExternalId(db: Db) {", + " for (let attempt = 0; attempt < 5; attempt++) {", + " const count = await db.file.count();", + " const externalId = count + 1;", + " try {", + " return await db.file.create({ data: { externalId } });", + " } catch (err: any) {", + " if (err.code !== 'P2002') throw err;", + " }", + " }", + " throw new Error('unique external ID collision retry exhausted');", + "}", + "interface Db { file: { count(): Promise; create(input: unknown): Promise } }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.integer-overflow") +} + +func TestDefensiveIntegerArithmeticSplitsSequenceAndMetricContexts(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/api/src/external-id.ts"), strings.Join([]string{ + "export async function allocateExternalId(db: Db) {", + " const count = await db.file.count();", + " const externalId = count + 1;", + " return db.file.create({ data: { externalId } });", + "}", + "export function recordMetric(stats: Stats, count: number, total: number) {", + " const nextTotal = total + count;", + " stats.histogram('documents_total').record(nextTotal);", + " return nextTotal;", + "}", + "interface Db { file: { count(): Promise; create(input: unknown): Promise } }", + "interface Stats { histogram(name: string): { record(value: number): void } }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRulePresent(t, report, "Code Quality", "defensive.sequence-collision-risk") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.integer-overflow") +} + +func TestDefensiveSequenceCollisionRecognizesExternalIDRetryHelper(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/api/src/external-id-retry.ts"), strings.Join([]string{ + "export async function allocateExternalId(db: Db) {", + " return withExternalIdRetry(async () => {", + " const count = await db.file.count();", + " const externalId = count + 1;", + " return db.file.create({ data: { externalId } });", + " }, { retryOn: 'P2002', field: 'externalId' });", + "}", + "declare function withExternalIdRetry(fn: () => Promise, opts: { retryOn: 'P2002'; field: 'externalId' }): Promise;", + "interface Db { file: { count(): Promise; create(input: unknown): Promise } }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.sequence-collision-risk") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.integer-overflow") +} + +func TestFunctionReturnContractAllowsNullableNextResponseGuards(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/app/api/files/guards.ts"), strings.Join([]string{ + "import { NextResponse } from 'next/server';", + "export function requireWorkspaceGuard(input: Input): NextResponse | null {", + " if (!input.workspaceId) {", + " return NextResponse.json({ error: 'missing workspace' }, { status: 400 });", + " }", + " return null;", + "}", + "interface Input { workspaceId?: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "function.inconsistent-return-contract") +} + +func TestDefensiveSchemaValidationRecognizesSchemaArgumentJSONReaders(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/api/src/json-reader.ts"), strings.Join([]string{ + "export function handleToolPayload(payload: string) {", + " const parsed = parseJson(payload, ToolActionSchema);", + " return parsed.action;", + "}", + "declare function parseJson(payload: string, schema: { parse(value: unknown): T }): T;", + "declare const ToolActionSchema: { parse(value: unknown): { action: string } };", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unvalidated-boundary-input") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-schema-validation") +} + +func TestDefensiveIntegerOverflowSkipsDateAndCountFormatting(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "apps/web/lib/date-formatting.ts"), strings.Join([]string{ + "export function formatDateBucket(dayCount: number) {", + " const displayDay = dayCount + 1;", + " return `Day ${displayDay}`;", + "}", + "export function formatCountLabel(count: number) {", + " const visibleCount = count + 1;", + " return visibleCount + ' items';", + "}", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.integer-overflow") + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.sequence-collision-risk") +} + +func TestDefensiveResourceLimitRecognizesSliceAndCountProofs(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/api/src/uploads.ts"), strings.Join([]string{ + "export async function readChunk(request: Request, maxCount: number) {", + " const raw = await request.body?.getReader().read();", + " const count = raw?.value?.length ?? 0;", + " if (count > maxCount) throw new Error('too many bytes');", + " return raw?.value?.slice(0, maxCount);", + "}", + "export async function uploadPreview(file: File) {", + " const preview = file.slice(0, INTERNAL_UPLOAD_MAX_BYTES);", + " await uploadClient.upload(preview);", + " return preview;", + "}", + "declare const INTERNAL_UPLOAD_MAX_BYTES: number;", + "declare const uploadClient: { upload(file: Blob): Promise };", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-resource-limit") +} diff --git a/tests/checks/quality_ui_false_positive_hardening_test.go b/tests/checks/quality_ui_false_positive_hardening_test.go index f550501..781ff28 100644 --- a/tests/checks/quality_ui_false_positive_hardening_test.go +++ b/tests/checks/quality_ui_false_positive_hardening_test.go @@ -439,158 +439,6 @@ func TestFunctionCommandQueryMixAllowsAPICommandReturningResult(t *testing.T) { } } -func TestQualityPrecisionAllowsDomainSideEffectAndAdapterOrchestrationNames(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "packages/connectors/src/abuse.ts"), strings.Join([]string{ - "export async function loadAbuseConfig(repo: Repo) {", - " const config = await repo.load();", - " await repo.update(config);", - " return config;", - "}", - "export async function evaluateActionAbuse(evaluator: Evaluator, action: Action) {", - " const result = await evaluator.evaluate(action);", - " await evaluator.record(result);", - " return result;", - "}", - "export async function maybeAlert(alerts: Alerts, result: Result) {", - " if (result.highRisk) await alerts.send(result);", - " return result;", - "}", - "export async function saveAbuseConfig(repo: Repo, input: Input) {", - " const parsed = parseInput(input);", - " await repo.save(parsed);", - " await repo.audit(parsed);", - " return parsed;", - "}", - "export async function postBugReportToSlack(slack: Slack, report: Report) {", - " const body = formatReport(report);", - " await slack.post(body);", - " await slack.record(body);", - " return body;", - "}", - "interface Repo { load(): Promise; update(input: unknown): Promise; save(input: unknown): Promise; audit(input: unknown): Promise }", - "interface Evaluator { evaluate(input: unknown): Promise; record(input: unknown): Promise }", - "interface Alerts { send(input: unknown): Promise }", - "interface Slack { post(input: unknown): Promise; record(input: unknown): Promise }", - "interface Input { id: string }", - "interface Action { id: string }", - "interface Result { highRisk: boolean }", - "interface Report { id: string }", - "declare function parseInput(input: Input): unknown;", - "declare function formatReport(report: Report): unknown;", - }, "\n")) - - report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) - - for _, ruleID := range []string{ - "function.hidden-mutation", - "function.multiple-responsibilities", - "function.mixed-abstraction-level", - "quality.mixed-abstraction-levels", - "smell.feature-envy", - } { - assertFindingRuleAbsent(t, report, "Code Quality", ruleID) - } -} - -func TestQualityNamingAllowsCommonConnectorNames(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "packages/connectors/src/normalizers.ts"), strings.Join([]string{ - "export function asRecord(value: unknown): Record {", - " return typeof value === 'object' && value !== null ? value as Record : {};", - "}", - "export function areStickerPlacementsEqual(source: Placement[], keys: string[], thresholds: Record, cached: boolean) {", - " const value = source.length === keys.length;", - " return cached && value && Object.keys(thresholds).length > 0;", - "}", - "interface Placement { id: string }", - }, "\n")) - - report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) - - assertFindingRuleAbsent(t, report, "Code Quality", "naming.boolean-not-predicate") - assertFindingRuleAbsent(t, report, "Code Quality", "naming.cardinality-mismatch") - assertFindingRuleAbsent(t, report, "Code Quality", "quality.ambiguous-name") -} - -func TestQualityDuplicatedKnowledgeSkipsTableAndEnumValueLiterals(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "packages/connectors/src/constants.ts"), strings.Join([]string{ - "export const abuseTableName = 'lmp_abuse_config';", - "export const auditTableName = 'lmp_abuse_config';", - "export const volumeOptions = [{ value: 'high_volume' }, { value: 'high_volume' }];", - "export const avatarOptions = [{ value: 'custom_avatar' }, { value: 'custom_avatar' }];", - }, "\n")) - - report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) - - assertFindingRuleAbsent(t, report, "Code Quality", "quality.duplicated-knowledge") -} - -func TestDefensiveRulesRecognizeValidationAndBoundedReadProofs(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "packages/connectors/src/boundary.ts"), strings.Join([]string{ - "export function handleToolAction(payload: Record) {", - " const parsed = parseToolActionPayload(payload);", - " return parsed.action;", - "}", - "export async function POST(request: Request) {", - " const contentLength = Number(request.headers.get('content-length') ?? '0');", - " if (contentLength > MAX_UPLOAD_BYTES) throw new Error('upload too large');", - " const form = await request.formData();", - " return Response.json({ ok: true, form });", - "}", - "export async function readBounded(response: Response) {", - " const bytes = await response.arrayBuffer();", - " if (bytes.byteLength > MAX_RESPONSE_BYTES) throw new Error('response too large');", - " return bytes;", - "}", - "export function normalizeWebhookUrl(input: string) {", - " const url = new URL(input);", - " if (url.protocol !== 'https:') throw new Error('invalid protocol');", - " return url;", - "}", - "function parseToolActionPayload(value: Record) {", - " const result = ToolActionSchema.safeParse(value);", - " if (!result.success) throw new Error('invalid payload');", - " return result.data;", - "}", - "declare const ToolActionSchema: { safeParse(value: unknown): { success: boolean; data: { action: string } } };", - "declare const MAX_UPLOAD_BYTES: number;", - "declare const MAX_RESPONSE_BYTES: number;", - }, "\n")) - - report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) - - assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unvalidated-boundary-input") - assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-schema-validation") - assertFindingRuleAbsent(t, report, "Code Quality", "defensive.missing-resource-limit") - assertFindingRuleAbsent(t, report, "Code Quality", "defensive.unchecked-external-response") -} - -func TestDefensiveIntegerOverflowSkipsGuardedP2002SequenceRetry(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "packages/api/src/external-id.ts"), strings.Join([]string{ - "export async function allocateExternalId(db: Db) {", - " for (let attempt = 0; attempt < 5; attempt++) {", - " const count = await db.file.count();", - " const externalId = count + 1;", - " try {", - " return await db.file.create({ data: { externalId } });", - " } catch (err: any) {", - " if (err.code !== 'P2002') throw err;", - " }", - " }", - " throw new Error('unique external ID collision retry exhausted');", - "}", - "interface Db { file: { count(): Promise; create(input: unknown): Promise } }", - }, "\n")) - - report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) - - assertFindingRuleAbsent(t, report, "Code Quality", "defensive.integer-overflow") -} - func TestReactNativeScreenAllowsUIBooleanAndLocalCollections(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "apps/mobile/src/screens/ClaimsScreen.tsx"), strings.Join([]string{