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
86 changes: 81 additions & 5 deletions internal/codeguard/checks/design/local_abstraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func leakFindings(env support.Context, file string, source string) []core.Findin
domainPath := isDomainPath(file)
apiPath := isAPIPath(file)
handlerPath := isHandlerPath(file)
persistenceBoundaryPath := domainPath || apiPath || handlerPath || isContractBoundaryPath(file)
for idx, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "#") {
Expand All @@ -121,10 +122,11 @@ 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) &&
!allowedGeneratedPersistenceEnumLine(trimmed) && !allowedTypeScriptRecordUtilityLine(trimmed) {
if persistenceBoundaryPath && (apiPath || handlerPath || isPublicDeclaration(trimmed)) && persistenceLeakPattern.MatchString(trimmed) &&
!allowedGeneratedPersistenceEnumLine(trimmed) && !allowedTypeScriptRecordUtilityLine(trimmed) &&
!allowedUIPropsDerivedTypeLine(file, trimmed) && !allowedFrameworkDTOBoundaryLine(file, trimmed) {
findings = append(findings, designFinding(env, rulePersistenceLeak, file, lineNo,
"persistence model or ORM concept leaks through a public/API boundary", core.ConfidenceHigh))
fmt.Sprintf("persistence model or ORM concept leaks through boundary at %s:%d: %s", file, lineNo, findingLineExcerpt(trimmed)), core.ConfidenceHigh))
}
if domainPath && configLeakPattern.MatchString(trimmed) {
findings = append(findings, designFinding(env, ruleConfigurationLeak, file, lineNo,
Expand All @@ -145,6 +147,35 @@ func allowedTypeScriptRecordUtilityLine(line string) bool {
!strings.Contains(line, "Row")
}

func allowedUIPropsDerivedTypeLine(file string, line string) bool {
if !isFrontendUIPath(file) {
return false
}
trimmed := strings.TrimSpace(line)
return strings.Contains(trimmed, "Props") ||
strings.Contains(trimmed, "ComponentProps") ||
strings.Contains(trimmed, "Pick<") ||
strings.Contains(trimmed, "Omit<") ||
strings.Contains(trimmed, "typeof ")
}

func allowedFrameworkDTOBoundaryLine(file string, line string) bool {
if !support.IsNestJSBoundaryPath(file) {
return false
}
trimmed := strings.TrimSpace(line)
if !strings.Contains(trimmed, "Dto") && !strings.Contains(trimmed, "DTO") {
return false
}
lowered := strings.ToLower(trimmed)
for _, blocked := range []string{"prismaclient", "prisma.", "typeorm", "sequelize", "sqlalchemy", "@entity", "entity", "model", "record", "row", "orm"} {
if strings.Contains(lowered, blocked) {
return false
}
}
return true
}

func allowedGeneratedPersistenceEnumLine(line string) bool {
lowered := strings.ToLower(line)
if !strings.Contains(lowered, "from") || !strings.Contains(lowered, "@prisma/client") {
Expand Down Expand Up @@ -441,8 +472,44 @@ func isDomainPath(file string) bool {

func isAPIPath(file string) bool {
normalized := strings.ToLower(filepathSlash(file))
return strings.Contains(normalized, "/api/") || strings.Contains(normalized, "/contract/") ||
strings.Contains(normalized, "/contracts/")
if strings.Contains(normalized, "/api/") {
return true
}
if isFrontendUIPath(file) {
return false
}
return strings.Contains(normalized, "/contract/")
}

func isContractBoundaryPath(file string) bool {
normalized := strings.ToLower(filepathSlash(file))
if isFrontendUIPath(file) {
return false
}
return strings.Contains(normalized, "/contracts/") ||
strings.Contains(normalized, "/dto/") ||
strings.Contains(normalized, "/schema/")
}

func isFrontendUIPath(file string) bool {
normalized := strings.ToLower(filepathSlash(file))
if strings.Contains(normalized, "/api/") ||
strings.Contains(normalized, "/server/") ||
strings.Contains(normalized, "/backend/") ||
strings.Contains(normalized, "/route.") {
return false
}
return strings.Contains(normalized, "/_components/") ||
strings.Contains(normalized, "/components/") ||
strings.Contains(normalized, "/screens/") ||
strings.Contains(normalized, "/navigation/") ||
strings.Contains(normalized, "/hooks/") ||
strings.Contains(normalized, "/packages/ui/") ||
strings.Contains(normalized, "/packages/design-system/") ||
strings.Contains(normalized, "/apps/mobile/") ||
strings.Contains(normalized, "/apps/native/") ||
strings.Contains(normalized, "/react-native/") ||
strings.Contains(normalized, "/app/") && strings.Contains(normalized, "web/")
}

func isHandlerPath(file string) bool {
Expand Down Expand Up @@ -485,3 +552,12 @@ func designFinding(env support.Context, ruleID string, file string, line int, me
Confidence: confidence,
})
}

func findingLineExcerpt(line string) string {
const maxExcerptLen = 120
line = strings.Join(strings.Fields(line), " ")
if len(line) <= maxExcerptLen {
return line
}
return line[:maxExcerptLen-1] + "…"
}
23 changes: 21 additions & 2 deletions internal/codeguard/checks/quality/quality_defensive.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ 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 := integerOverflowLine(fn, loweredBody); ok {
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))
}
Expand Down Expand Up @@ -182,7 +182,10 @@ func firstUseLine(fn precisionFunction, name string) int {
return fn.StartLine
}

func integerOverflowLine(fn precisionFunction, loweredBody string) (int, bool) {
func integerOverflowLine(file string, fn precisionFunction, loweredBody string) (int, bool) {
if isUIRenderArithmeticContext(file, fn, loweredBody) {
return 0, false
}
if containsAny(loweredBody, []string{"maxint", "math.max", "checked", "saturating", "overflow", "limits<", "safeint"}) {
return 0, false
}
Expand All @@ -195,6 +198,22 @@ func integerOverflowLine(fn precisionFunction, loweredBody string) (int, bool) {
return 0, false
}

func isUIRenderArithmeticContext(file string, fn precisionFunction, loweredBody string) bool {
if isUIHelperOrMappingContext(file, fn) {
return true
}
if !isScriptLikeSourcePath(file) || !isLikelyUIFile(file) {
return false
}
if isUIRenderHelperName(fn.Name) || isUIRenderMappingBody(fn.Body) {
return true
}
return containsAny(loweredBody, []string{
"stylesheet.", "dimensions.", "pixelratio.", "fontscale", "spacing",
"padding", "margin", "width", "height", "opacity", "zindex",
})
}

func boundsAssumptionLine(fn precisionFunction, loweredBody string) (int, bool) {
if containsAny(loweredBody, []string{"len(", ".length", ".size()", "empty()", "bounds", "range", "count >"}) {
return 0, false
Expand Down
50 changes: 28 additions & 22 deletions internal/codeguard/checks/quality/quality_precision.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,11 @@ type precisionFunction struct {
Assignments []support.ParsedAssignment
Calls []support.ParsedCall
Statements []support.ParsedStatement
Nested []precisionLineRange
Body string
Returns bool
}

func precisionWarnFinding(env support.Context, ruleID string, file string, line int, message string, confidence string) core.Finding {
return env.NewFinding(support.FindingInput{
RuleID: ruleID,
Level: "warn",
Path: file,
Line: line,
Column: 1,
Message: message,
Confidence: confidence,
})
}

func localPrecisionEnabled(env support.Context) bool {
return env.Config.Checks.QualityRules.LocalPrecision == nil || *env.Config.Checks.QualityRules.LocalPrecision
}
Expand Down Expand Up @@ -271,7 +260,7 @@ func goDuplicatedKnowledgeFindings(env support.Context, file string, fset *token
}
if first, exists := seen[lit.Value]; exists {
return []core.Finding{precisionWarnFinding(env, qualityDuplicatedKnowledgeRuleID, file, fset.Position(expr.Pos()).Line,
fmt.Sprintf("business literal is duplicated near line %d; centralize shared domain knowledge", fset.Position(first).Line), core.ConfidenceLow)}
fmt.Sprintf("business literal %s is duplicated near line %d; centralize shared domain knowledge", lit.Value, fset.Position(first).Line), core.ConfidenceLow)}
}
seen[lit.Value] = expr.Pos()
}
Expand Down Expand Up @@ -369,6 +358,7 @@ func parsedPrecisionFunction(fn *support.ParsedFunction) precisionFunction {
Assignments: fn.Assignments,
Calls: fn.Calls,
Statements: fn.Statements,
Nested: nestedPrecisionLineRanges(fn),
Body: body,
Returns: strings.Contains(body, "return "),
}
Expand Down Expand Up @@ -426,7 +416,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, qualityPrimitiveObsessionRuleID, file, fn.StartLine,
fmt.Sprintf("function %s passes several domain concepts as raw primitives", fn.Name), core.ConfidenceMedium))
}
if hiddenSideEffect(fn) {
if hiddenSideEffect(file, fn) {
findings = append(findings, precisionWarnFinding(env, qualityHiddenSideEffectRuleID, file, fn.StartLine,
fmt.Sprintf("function %s name implies a query/build operation but it performs side effects", fn.Name), core.ConfidenceMedium))
}
Expand Down Expand Up @@ -478,12 +468,16 @@ func primitiveObsession(fn precisionFunction) bool {
return count >= 3
}

func hiddenSideEffect(fn precisionFunction) bool {
func hiddenSideEffect(file string, fn precisionFunction) bool {
if isFrameworkOrchestrationBoundary(file, fn) {
return false
}
if !queryFunctionPrefixPattern.MatchString(strings.ToLower(fn.Name)) {
return false
}
for _, call := range fn.Calls {
if mutatingCallPattern.MatchString(call.Callee) {
localTargets := localMutationTargets(fn)
for _, call := range directCalls(fn) {
if mutatingCallPattern.MatchString(call.Callee) && !isLocalMutationCall(call.Callee, localTargets) {
return true
}
}
Expand Down Expand Up @@ -517,7 +511,7 @@ func isDomainLevelCall(callee string) bool {
}

func commandQueryMix(file string, fn precisionFunction) bool {
if isFrameworkCommandBoundary(file, fn.Name) || isReactComponentOrHookBoundary(file, fn) {
if isFrameworkOrchestrationBoundary(file, fn) || isReactComponentOrHookBoundary(file, fn) {
return false
}
if !fn.Returns {
Expand All @@ -527,8 +521,9 @@ func commandQueryMix(file string, fn precisionFunction) bool {
if !queryFunctionPrefixPattern.MatchString(name) && !strings.Contains(fn.Body, "return ") {
return false
}
for _, call := range fn.Calls {
if mutatingCallPattern.MatchString(call.Callee) {
localTargets := localMutationTargets(fn)
for _, call := range directCalls(fn) {
if mutatingCallPattern.MatchString(call.Callee) && !isLocalMutationCall(call.Callee, localTargets) {
return true
}
}
Expand Down Expand Up @@ -759,15 +754,26 @@ func domainKnowledgeLiteral(value string) bool {
if len(trimmed) < 4 && !strings.ContainsAny(trimmed, "0123456789") {
return false
}
if _, err := strconv.Atoi(trimmed); err == nil {
return true
if numeric, ok := duplicatedKnowledgeNumber(trimmed); ok {
return numeric >= 10
}
if likelyDisplayLabel(trimmed) {
return false
}
return domainPrimitiveNamePattern.MatchString(trimmed) || strings.Contains(trimmed, "_")
}

func duplicatedKnowledgeNumber(value string) (int, bool) {
number, err := strconv.Atoi(value)
if err != nil {
return 0, false
}
if number < 0 {
number = -number
}
return number, true
}

func likelyDisplayLabel(value string) bool {
if strings.Contains(value, "_") {
return false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package quality

import (
"strings"

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

func isFrameworkOrchestrationBoundary(file string, fn precisionFunction) bool {
return isFrameworkCommandBoundary(file, fn.Name) || isNestJSRequestBoundary(file)
}

func isFrameworkConventionalAmbiguousName(file string, fn precisionFunction, name string) bool {
if !isFrameworkOrchestrationBoundary(file, fn) {
return false
}
switch strings.ToLower(strings.Trim(name, "_$")) {
case "data", "value", "values", "item", "items":
return true
default:
return false
}
}

func isNestJSRequestBoundary(file string) bool {
return isScriptLikeSourcePath(file) && support.IsNestJSBoundaryPath(file)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var localAccumulatorExprPattern = regexp.MustCompile(`(?i)^(?:new\s+)?(?:array|f
func localMutationTargets(fn precisionFunction) map[string]struct{} {
params := paramNames(fn)
targets := make(map[string]struct{})
for _, assignment := range fn.Assignments {
for _, assignment := range directAssignments(fn) {
name := strings.TrimSpace(assignment.Name)
if name == "" || assignment.Augmented {
continue
Expand All @@ -30,6 +30,54 @@ func localMutationTargets(fn precisionFunction) map[string]struct{} {
return targets
}

func directAssignments(fn precisionFunction) []support.ParsedAssignment {
if len(fn.Nested) == 0 {
return fn.Assignments
}
assignments := make([]support.ParsedAssignment, 0, len(fn.Assignments))
for _, assignment := range fn.Assignments {
if !callInNestedFunction(fn, assignment.Line) {
assignments = append(assignments, assignment)
}
}
return assignments
}

func directCalls(fn precisionFunction) []support.ParsedCall {
if len(fn.Nested) == 0 {
return fn.Calls
}
calls := make([]support.ParsedCall, 0, len(fn.Calls))
for _, call := range fn.Calls {
if !callInNestedFunction(fn, call.Line) {
calls = append(calls, call)
}
}
return calls
}

func directStatements(fn precisionFunction) []support.ParsedStatement {
if len(fn.Nested) == 0 {
return fn.Statements
}
statements := make([]support.ParsedStatement, 0, len(fn.Statements))
for _, statement := range fn.Statements {
if !callInNestedFunction(fn, statement.Line) {
statements = append(statements, statement)
}
}
return statements
}

func callInNestedFunction(fn precisionFunction, line int) bool {
for _, nested := range fn.Nested {
if line >= nested.Start && line <= nested.End {
return true
}
}
return false
}

func assignmentLooksLocalAccumulator(fn precisionFunction, assignment support.ParsedAssignment) bool {
expr := strings.TrimSpace(assignment.Expr)
if localAccumulatorExprPattern.MatchString(expr) {
Expand Down Expand Up @@ -81,7 +129,7 @@ func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool {

func isBareLocalMutationCall(callee string) bool {
switch strings.TrimSpace(callee) {
case "append", "Set":
case "append", "Set", "Array", "Object", "Map", "WeakMap", "WeakSet":
return true
default:
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ func isReactHookStateBoundary(file string, fn precisionFunction) bool {
return false
}

func isReactLocalStateBoundary(file string, fn precisionFunction) bool {
if isReactHookStateBoundary(file, fn) {
return true
}
return isReactComponentOrHookBoundary(file, fn) && callsReactLocalStateSetter(fn)
}

func isScriptLikeSourcePath(file string) bool {
lowered := strings.ToLower(file)
return strings.HasSuffix(lowered, ".ts") || strings.HasSuffix(lowered, ".tsx") ||
Expand Down
Loading
Loading