Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion internal/codeguard/checks/design/local_abstraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ func leakFindings(env support.Context, file string, source string) []core.Findin
findings = append(findings, designFinding(env, ruleInfrastructureLeak, file, lineNo,
"infrastructure/framework type leaks into a domain or public boundary", core.ConfidenceHigh))
}
if (apiPath || handlerPath || isPublicDeclaration(trimmed)) && persistenceLeakPattern.MatchString(trimmed) {
if (apiPath || handlerPath || isPublicDeclaration(trimmed)) && persistenceLeakPattern.MatchString(trimmed) &&
!allowedGeneratedPersistenceEnumLine(trimmed) && !allowedTypeScriptRecordUtilityLine(trimmed) {
findings = append(findings, designFinding(env, rulePersistenceLeak, file, lineNo,
"persistence model or ORM concept leaks through a public/API boundary", core.ConfidenceHigh))
}
Expand All @@ -136,6 +137,54 @@ func leakFindings(env support.Context, file string, source string) []core.Findin
return firstFindingPerRule(findings)
}

func allowedTypeScriptRecordUtilityLine(line string) bool {
return strings.Contains(line, "Record<") &&
!strings.Contains(line, "PrismaClient") &&
!strings.Contains(line, "Model") &&
!strings.Contains(line, "Entity") &&
!strings.Contains(line, "Row")
}

func allowedGeneratedPersistenceEnumLine(line string) bool {
lowered := strings.ToLower(line)
if !strings.Contains(lowered, "from") || !strings.Contains(lowered, "@prisma/client") {
return false
}
if strings.Contains(line, "PrismaClient") || strings.Contains(line, "Prisma.") {
return false
}
open := strings.Index(line, "{")
closeBrace := strings.Index(line, "}")
if open < 0 || closeBrace <= open {
return false
}
for _, part := range strings.Split(line[open+1:closeBrace], ",") {
name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(part), "type "))
if name == "" {
continue
}
if strings.Contains(strings.ToLower(name), " as ") {
name = strings.TrimSpace(strings.SplitN(name, " as ", 2)[0])
}
if !looksLikeGeneratedEnumType(name) {
return false
}
}
return true
}

func looksLikeGeneratedEnumType(name string) bool {
if strings.Contains(name, "Client") || strings.Contains(name, "Model") || strings.Contains(name, "Record") ||
strings.Contains(name, "Row") || strings.Contains(name, "Entity") {
return false
}
if name == "" {
return false
}
first := rune(name[0])
return first >= 'A' && first <= 'Z'
}

func domainLogicHandlerFinding(env support.Context, file string, lines []string) []core.Finding {
score := 0
lineNo := 1
Expand Down
58 changes: 51 additions & 7 deletions internal/codeguard/checks/quality/quality_precision.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, namingGenericIdentifierRuleID, file, fn.StartLine,
fmt.Sprintf("function name %q is too generic to communicate intent", fn.Name), core.ConfidenceHigh))
}
if isAmbiguousIdentifier(fn.Name) {
if isAmbiguousIdentifier(fn.Name) && !isUIConventionalAmbiguousName(file, fn, fn.Name, "", fn.StartLine) {
findings = append(findings, precisionWarnFinding(env, qualityAmbiguousNameRuleID, file, fn.StartLine,
fmt.Sprintf("function name %q is ambiguous without domain context", fn.Name), core.ConfidenceHigh))
}
Expand All @@ -392,7 +392,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, namingGenericIdentifierRuleID, file, fn.StartLine,
fmt.Sprintf("parameter %q is too generic to communicate intent", param.Name), core.ConfidenceHigh))
}
if isAmbiguousIdentifier(param.Name) {
if isAmbiguousIdentifier(param.Name) && !isUIConventionalAmbiguousName(file, fn, param.Name, param.Type, fn.StartLine) {
findings = append(findings, precisionWarnFinding(env, qualityAmbiguousNameRuleID, file, fn.StartLine,
fmt.Sprintf("parameter %q is ambiguous without domain context", param.Name), core.ConfidenceHigh))
}
Expand All @@ -406,7 +406,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, namingGenericIdentifierRuleID, file, assignment.Line,
fmt.Sprintf("identifier %q is too generic to explain its role", assignment.Name), core.ConfidenceHigh))
}
if isAmbiguousIdentifier(assignment.Name) {
if isAmbiguousIdentifier(assignment.Name) && !isUIConventionalAmbiguousName(file, fn, assignment.Name, "", assignment.Line) {
findings = append(findings, precisionWarnFinding(env, qualityAmbiguousNameRuleID, file, assignment.Line,
fmt.Sprintf("identifier %q is ambiguous without domain context", assignment.Name), core.ConfidenceHigh))
}
Expand All @@ -417,7 +417,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, qualityMixedAbstractionLevelsRuleID, file, fn.StartLine,
fmt.Sprintf("function %s mixes domain intent with low-level implementation details", fn.Name), core.ConfidenceMedium))
}
if commandQueryMix(fn) {
if commandQueryMix(file, fn) {
findings = append(findings, precisionWarnFinding(env, functionCommandQueryMixRuleID, file, fn.StartLine,
fmt.Sprintf("function %s returns a value while also invoking mutating side-effect operations", fn.Name), core.ConfidenceMedium))
}
Expand Down Expand Up @@ -516,7 +516,10 @@ func isDomainLevelCall(callee string) bool {
return strings.Contains(callee, ".") || queryFunctionPrefixPattern.MatchString(lowered) || len(callee) > 3
}

func commandQueryMix(fn precisionFunction) bool {
func commandQueryMix(file string, fn precisionFunction) bool {
if isFrameworkCommandBoundary(file, fn.Name) || isReactComponentOrHookBoundary(file, fn) {
return false
}
if !fn.Returns {
return false
}
Expand Down Expand Up @@ -622,6 +625,9 @@ func parsedMutableGlobalFindings(env support.Context, file string, parsed *suppo
if text == "" || strings.HasPrefix(text, "const ") || strings.HasPrefix(text, "final ") {
continue
}
if isScriptLikeSourcePath(file) && !moduleStatementLooksTopLevel(statement) {
continue
}
if mutableGlobalLinePattern.MatchString(text) {
findings = append(findings, precisionWarnFinding(env, qualityMutableGlobalStateRuleID, file, statement.Line,
"mutable module-level state makes behavior harder to isolate and test", core.ConfidenceHigh))
Expand All @@ -639,7 +645,7 @@ func parsedDuplicatedKnowledgeFindings(env support.Context, file string, parsed
for _, literal := range domainKnowledgeLiterals(statement.Raw) {
if first, exists := seen[literal]; exists {
return []core.Finding{precisionWarnFinding(env, qualityDuplicatedKnowledgeRuleID, file, statement.Line,
fmt.Sprintf("business literal is duplicated near line %d; centralize shared domain knowledge", first), core.ConfidenceLow)}
fmt.Sprintf("business literal %s is duplicated near line %d; centralize shared domain knowledge", literal, first), core.ConfidenceLow)}
}
seen[literal] = statement.Line
}
Expand Down Expand Up @@ -672,6 +678,9 @@ func sourceMutableGlobalFindings(env support.Context, file string, source string
return nil
}
for idx, line := range strings.Split(strings.ReplaceAll(source, "\r\n", "\n"), "\n") {
if isScriptLikeSourcePath(file) && !scriptSourceLineAtModuleScope(source, idx) {
continue
}
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "#") ||
strings.HasPrefix(trimmed, "const ") || strings.HasPrefix(trimmed, "final ") {
Expand All @@ -697,7 +706,7 @@ func sourceDuplicatedKnowledgeFindings(env support.Context, file string, source
for _, literal := range domainKnowledgeLiterals(line) {
if first, exists := seen[literal]; exists {
return []core.Finding{precisionWarnFinding(env, qualityDuplicatedKnowledgeRuleID, file, idx+1,
fmt.Sprintf("business literal is duplicated near line %d; centralize shared domain knowledge", first), core.ConfidenceLow)}
fmt.Sprintf("business literal %s is duplicated near line %d; centralize shared domain knowledge", literal, first), core.ConfidenceLow)}
}
seen[literal] = idx + 1
}
Expand All @@ -714,6 +723,9 @@ func redundantCommentVerb(comment string) string {
}

func domainKnowledgeLiterals(line string) []string {
if duplicatedKnowledgeLineIsDisplayOnly(line) {
return nil
}
matches := regexp.MustCompile(`"([^"]{2,80})"|'([^']{2,80})'|\b\d+(?:\.\d+)?\b`).FindAllString(line, -1)
out := make([]string, 0, len(matches))
for _, match := range matches {
Expand All @@ -724,17 +736,49 @@ func domainKnowledgeLiterals(line string) []string {
return out
}

func duplicatedKnowledgeLineIsDisplayOnly(line string) bool {
lowered := strings.ToLower(line)
if strings.Contains(lowered, "classname") || strings.Contains(lowered, "clasname") || strings.Contains(lowered, "class:") {
return true
}
if strings.Contains(line, "<") && strings.Contains(line, ">") {
return true
}
if strings.Contains(lowered, "label:") || strings.Contains(lowered, "placeholder:") || strings.Contains(lowered, "title:") ||
strings.Contains(lowered, "aria-label") {
return true
}
return false
}

func domainKnowledgeLiteral(value string) bool {
trimmed := strings.Trim(value, `"'`)
if trimmed == "" || len(trimmed) > 80 {
return false
}
if len(trimmed) < 4 && !strings.ContainsAny(trimmed, "0123456789") {
return false
}
if _, err := strconv.Atoi(trimmed); err == nil {
return true
}
if likelyDisplayLabel(trimmed) {
return false
}
return domainPrimitiveNamePattern.MatchString(trimmed) || strings.Contains(trimmed, "_")
}

func likelyDisplayLabel(value string) bool {
if strings.Contains(value, "_") {
return false
}
if strings.ContainsAny(value, "-/:.") {
return false
}
words := strings.Fields(value)
return len(words) > 0 && len(words) <= 3
}

func unsafeScriptNumericConversion(text string) bool {
lowered := strings.ToLower(text)
return strings.Contains(lowered, "static_cast<int8_t>") ||
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package quality

import (
"path/filepath"
"regexp"
"strings"

"github.com/devr-tools/codeguard/internal/codeguard/checks/support"
)

var conventionalMutationBoundaryPattern = regexp.MustCompile(`^(accept|apply|approve|archive|clear|close|commit|deliver|download|drop|ensure|exists|fetch|import|list|notify|open|process|read|reconcile|record|run|seed|submit|sync|toggle|upload)`)

var localAccumulatorExprPattern = regexp.MustCompile(`(?i)^(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b|^\[\]|^\{\}|^make\s*\(|^(?:bytes|strings)\.buffer\b|^strings\.builder\b`)

func localMutationTargets(fn precisionFunction) map[string]struct{} {
params := paramNames(fn)
targets := make(map[string]struct{})
for _, assignment := range fn.Assignments {
name := strings.TrimSpace(assignment.Name)
if name == "" || assignment.Augmented {
continue
}
if _, isParam := params[name]; isParam {
continue
}
if assignmentLooksLocalAccumulator(fn, assignment) {
targets[name] = struct{}{}
}
}
return targets
}

func assignmentLooksLocalAccumulator(fn precisionFunction, assignment support.ParsedAssignment) bool {
expr := strings.TrimSpace(assignment.Expr)
if localAccumulatorExprPattern.MatchString(expr) {
return true
}
statement := assignmentStatement(fn, assignment.Line)
if statement == "" {
return false
}
name := regexp.QuoteMeta(assignment.Name)
return regexp.MustCompile(`(?i)\b(?:const|let|var)\s+`+name+`\b.*=\s*(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b`).MatchString(statement) ||
regexp.MustCompile(`(?i)\bvar\s+`+name+`\s+(?:bytes\.buffer|strings\.builder)\b`).MatchString(statement) ||
regexp.MustCompile(`(?i)\bstd::(?:vector|map|set|unordered_map|unordered_set|stringstream)\b[^;\n]*\b`+name+`\b`).MatchString(statement) ||
regexp.MustCompile(`\b`+name+`\s*:=\s*(?:\[\]|\{\}|make\s*\(|(?:bytes|strings)\.Buffer\b|strings\.Builder\b)`).MatchString(statement)
}

func assignmentStatement(fn precisionFunction, line int) string {
for _, statement := range fn.Statements {
if statement.Line == line {
if strings.TrimSpace(statement.Raw) != "" {
return statement.Raw
}
return statement.Text
}
}
return ""
}

func paramNames(fn precisionFunction) map[string]struct{} {
params := make(map[string]struct{}, len(fn.Params))
for _, param := range fn.Params {
if param.Name != "" {
params[param.Name] = struct{}{}
}
}
return params
}

func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool {
if isBareLocalMutationCall(callee) {
return true
}
target := mutationCallTarget(callee)
if target == "" {
return false
}
return isLocalMutationTarget(target, localTargets)
}

func isBareLocalMutationCall(callee string) bool {
switch strings.TrimSpace(callee) {
case "append", "Set":
return true
default:
return false
}
}

func mutationCallTarget(callee string) string {
callee = strings.TrimSpace(callee)
if callee == "" {
return ""
}
for _, sep := range []string{".", "->", "::"} {
if idx := strings.Index(callee, sep); idx > 0 {
return strings.TrimSpace(callee[:idx])
}
}
return ""
}

func isLocalMutationTarget(name string, localTargets map[string]struct{}) bool {
_, ok := localTargets[strings.TrimSpace(name)]
return ok
}

func isFrameworkCommandBoundary(file string, name string) bool {
if !isScriptLikeSourcePath(file) {
return false
}
if !isHTTPMethodName(name) {
return false
}
normalized := strings.ReplaceAll(file, "\\", "/")
return strings.HasSuffix(normalized, "/route.ts") || strings.HasSuffix(normalized, "/route.tsx") ||
strings.HasSuffix(normalized, "/route.js") || strings.HasSuffix(normalized, "/route.jsx")
}

func isHTTPMethodName(name string) bool {
switch strings.ToUpper(strings.TrimSpace(name)) {
case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS":
return true
default:
return false
}
}

func isScriptEntrypoint(file string, name string) bool {
if strings.TrimSpace(name) != "main" {
return false
}
normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/"))
base := filepath.Base(normalized)
if strings.HasPrefix(base, "seed") || strings.HasPrefix(base, "backfill") || strings.HasPrefix(base, "import") || strings.HasPrefix(base, "cleanup") {
return true
}
return strings.Contains(normalized, "/scripts/") ||
strings.Contains(normalized, "/script/") ||
strings.Contains(normalized, "/backfill") ||
strings.Contains(normalized, "/seed") ||
strings.Contains(normalized, "/import")
}
Loading
Loading