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
33 changes: 33 additions & 0 deletions internal/codeguard/checks/quality/quality_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var (
cleanupIgnoredPattern = regexp.MustCompile(`(?i)(_\s*=\s*[^;\n]*(close|rollback|remove|delete)\s*\(|defer\s+[^;\n]*\.close\s*\(|catch\s*\([^)]*\)\s*\{\s*(?:/\*.*\*/|//.*)?\s*\})`)
panicPattern = regexp.MustCompile(`\bpanic\s*\(`)
throwRaisePattern = regexp.MustCompile(`(?i)\b(throw|raise)\b`)
nonPartialResultReturnPattern = regexp.MustCompile(`(?i)^return(?:\s+(.+?))?\s*;?$`)
)

func errorContractFindings(env support.Context, file string, fn precisionFunction) []core.Finding {
Expand Down Expand Up @@ -198,6 +199,9 @@ func cleanupIgnoredLine(statements []support.ParsedStatement) (int, bool) {
}

func partialFailureHiddenLine(fn precisionFunction, loweredBody string) (int, bool) {
if nonPartialResultFunction(fn) {
return 0, false
}
if partialFailureSurfacedInResult(loweredBody) {
return 0, false
}
Expand Down Expand Up @@ -225,6 +229,35 @@ func partialFailureHiddenLine(fn precisionFunction, loweredBody string) (int, bo
return 0, false
}

func nonPartialResultFunction(fn precisionFunction) bool {
signature := strings.ToLower(strings.ReplaceAll(fn.Signature, " ", ""))
if !fn.Returns {
return true
}
switch signature {
case "error", "(error)", "void":
return true
}
if strings.Contains(signature, "promise<void>") {
return true
}
for _, statement := range fn.Statements {
line := strings.TrimSpace(firstNonEmptyString(statement.Raw, statement.Text))
match := nonPartialResultReturnPattern.FindStringSubmatch(line)
if len(match) == 0 {
continue
}
if len(match) == 1 || strings.TrimSpace(match[1]) == "" {
continue
}
value := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(match[1], ";")))
if value != "nil" && value != "none" && value != "null" && value != "undefined" {
return false
}
}
return signature == "" || signature == "none"
}

func allSettledResultIsReturned(loweredBody string) bool {
return containsAny(loweredBody, []string{
"return {", "normalize", "mapsettled", "settledresults", "fulfilled", "rejected",
Expand Down
22 changes: 22 additions & 0 deletions internal/codeguard/checks/quality/quality_precision.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func goPrecisionFunction(fset *token.FileSet, fn *ast.FuncDecl, data []byte) pre
Name: fn.Name.Name,
StartLine: fset.Position(fn.Pos()).Line,
EndLine: fset.Position(fn.End()).Line,
Signature: goResultSignature(fn),
Params: goParsedParams(fn),
Returns: goFuncReturnsValue(fn),
}
Expand Down Expand Up @@ -148,6 +149,27 @@ func goPrecisionFunction(fset *token.FileSet, fn *ast.FuncDecl, data []byte) pre
return out
}

func goResultSignature(fn *ast.FuncDecl) string {
if fn.Type == nil || fn.Type.Results == nil || len(fn.Type.Results.List) == 0 {
return ""
}
results := make([]string, 0, len(fn.Type.Results.List))
for _, field := range fn.Type.Results.List {
text := goExprText(field.Type)
if len(field.Names) == 0 {
results = append(results, text)
continue
}
for range field.Names {
results = append(results, text)
}
}
if len(results) == 1 {
return results[0]
}
return "(" + strings.Join(results, ", ") + ")"
}

func goParsedParams(fn *ast.FuncDecl) []support.ParsedParam {
if fn.Type == nil || fn.Type.Params == nil {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

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

func isFrameworkConventionalAmbiguousName(file string, fn precisionFunction, name string) bool {
Expand Down Expand Up @@ -44,3 +44,31 @@ func isTrackedRouteBoundary(file string, fn precisionFunction) bool {
body := strings.ToLower(fn.Body)
return strings.Contains(body, "nextresponse.") || strings.Contains(body, "response.json(")
}

func isGoRouterRegistrationBoundary(file string, fn precisionFunction) bool {
if !strings.HasSuffix(strings.ToLower(file), ".go") {
return false
}
loweredName := strings.ToLower(strings.Trim(fn.Name, "_$"))
if loweredName == "registerroutes" || strings.HasPrefix(loweredName, "add") && strings.Contains(loweredName, "routes") {
return hasRouterRegistrationEvidence(fn)
}
return false
}

func hasRouterRegistrationEvidence(fn precisionFunction) bool {
signature := strings.ToLower(fn.Signature)
if strings.Contains(signature, "chi.router") || strings.Contains(signature, "mux.router") || strings.Contains(signature, "gin.engine") {
return true
}
for _, call := range fn.Calls {
lowered := strings.ToLower(call.Callee)
if strings.HasSuffix(lowered, ".route") || strings.HasSuffix(lowered, ".get") ||
strings.HasSuffix(lowered, ".post") || strings.HasSuffix(lowered, ".put") ||
strings.HasSuffix(lowered, ".patch") || strings.HasSuffix(lowered, ".delete") ||
strings.HasSuffix(lowered, ".handle") || strings.HasSuffix(lowered, ".handlefunc") {
return true
}
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ var (
partialFailureContinuePattern = regexp.MustCompile(`^\s*continue\s*;?\s*(?://.*)?$`)
partialFailureSuccessReturn = regexp.MustCompile(`(?i)^\s*return(?:\s+(?:nil|none|null|true|0|\{\}))?\s*;?\s*$`)
partialFailurePropagatePattern = regexp.MustCompile(`(?i)\b(return\s+err|return\s+error|raise\b|throw\b)`)
partialFailureGoErrorFunc = regexp.MustCompile(`^\s*func\s+(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(?:error|\(\s*error\s*\))\s*\{`)
partialFailureScriptVoidFunc = regexp.MustCompile(`\bfunction\s+[A-Za-z_$][\w$]*\s*\([^)]*\)\s*:\s*(?:Promise\s*<\s*void\s*>|void)\s*\{`)
partialFailureCPPVoidFunc = regexp.MustCompile(`^\s*(?:[\w:<>]+\s+)*void\s+[A-Za-z_]\w*\s*\([^)]*\)\s*(?:const\s*)?\{`)
partialFailurePythonDef = regexp.MustCompile(`^\s*def\s+[A-Za-z_]\w*\s*\([^)]*\)\s*(?:->\s*None\s*)?:`)
partialFailureValuedReturn = regexp.MustCompile(`(?i)^\s*return\s+(.+?)\s*;?\s*$`)
)

func partialFailureHiddenFindings(env support.Context, file string, data []byte) []core.Finding {
if !enabled(env.Config.Checks.ReliabilityRules.DetectPartialFailureHidden) {
return nil
}
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
contracts := partialFailureNonResultFunctionLines(lines)
loopDepth := 0
pending := 0
findings := make([]core.Finding, 0, 1)
Expand All @@ -39,12 +45,16 @@ func partialFailureHiddenFindings(env support.Context, file string, data []byte)
pending = lineNo
}
if pending > 0 && partialFailureContinuePattern.MatchString(trimmed) {
findings = append(findings, partialFailureFinding(env, file, pending, "logged failure is skipped and batch processing continues without surfacing partial failure"))
if !contracts[pending] {
findings = append(findings, partialFailureFinding(env, file, pending, "logged failure is skipped and batch processing continues without surfacing partial failure"))
}
pending = 0
continue
}
if pending > 0 && lineNo <= pending+12 && partialFailureSuccessReturn.MatchString(trimmed) {
findings = append(findings, partialFailureFinding(env, file, pending, "logged failure is followed by a success return, hiding partial failure from callers"))
if !contracts[pending] {
findings = append(findings, partialFailureFinding(env, file, pending, "logged failure is followed by a success return, hiding partial failure from callers"))
}
pending = 0
continue
}
Expand All @@ -57,6 +67,76 @@ func partialFailureHiddenFindings(env support.Context, file string, data []byte)
})
}

func partialFailureNonResultFunctionLines(lines []string) map[int]bool {
out := map[int]bool{}
for start := 0; start < len(lines); start++ {
line := lines[start]
switch {
case partialFailureGoErrorFunc.MatchString(line) || partialFailureScriptVoidFunc.MatchString(line) || partialFailureCPPVoidFunc.MatchString(line):
end := partialFailureBraceFunctionEnd(lines, start)
for idx := start + 1; idx <= end && idx < len(lines); idx++ {
out[idx+1] = true
}
start = end
case partialFailurePythonDef.MatchString(line):
end := partialFailurePythonFunctionEnd(lines, start)
if partialFailurePythonReturnsOnlyNone(lines[start+1 : end+1]) {
for idx := start + 1; idx <= end && idx < len(lines); idx++ {
out[idx+1] = true
}
}
start = end
}
}
return out
}

func partialFailureBraceFunctionEnd(lines []string, start int) int {
depth := 0
seenOpen := false
for idx := start; idx < len(lines); idx++ {
line := lines[idx]
depth += strings.Count(line, "{")
if strings.Contains(line, "{") {
seenOpen = true
}
depth -= strings.Count(line, "}")
if seenOpen && depth <= 0 {
return idx
}
}
return len(lines) - 1
}

func partialFailurePythonFunctionEnd(lines []string, start int) int {
baseIndent := len(lines[start]) - len(strings.TrimLeft(lines[start], " \t"))
for idx := start + 1; idx < len(lines); idx++ {
trimmed := strings.TrimSpace(lines[idx])
if trimmed == "" {
continue
}
indent := len(lines[idx]) - len(strings.TrimLeft(lines[idx], " \t"))
if indent <= baseIndent {
return idx - 1
}
}
return len(lines) - 1
}

func partialFailurePythonReturnsOnlyNone(lines []string) bool {
for _, line := range lines {
match := partialFailureValuedReturn.FindStringSubmatch(strings.TrimSpace(line))
if len(match) != 2 {
continue
}
value := strings.TrimSpace(strings.TrimSuffix(match[1], ";"))
if !strings.EqualFold(value, "none") {
return false
}
}
return true
}

func partialFailureFinding(env support.Context, file string, line int, message string) core.Finding {
return newFinding(env, "reliability.partial-failure-hidden", "fail", file, line, 1, message, "medium", "failure_mode", "partial-failure-hidden")
}
6 changes: 4 additions & 2 deletions tests/checks/quality_error_defensive_multilang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,15 @@ func TestQualityErrorContractsDetectMultiLanguageSignals(t *testing.T) {
"\treturn nil",
"}",
"",
"func ProcessAll(items []string) error {",
"func ProcessAll(items []string) ([]string, error) {",
"\tprocessed := make([]string, 0, len(items))",
"\tfor _, item := range items {",
"\t\tif err := sendItem(item); err != nil {",
"\t\t\tcontinue",
"\t\t}",
"\t\tprocessed = append(processed, item)",
"\t}",
"\treturn nil",
"\treturn processed, nil",
"}",
"",
"func DecodeConfig(raw []byte) map[string]string {",
Expand Down
Loading
Loading