diff --git a/internal/codeguard/checks/quality/quality_defensive.go b/internal/codeguard/checks/quality/quality_defensive.go index 827303a..110d609 100644 --- a/internal/codeguard/checks/quality/quality_defensive.go +++ b/internal/codeguard/checks/quality/quality_defensive.go @@ -318,8 +318,6 @@ func nullableParamGuarded(loweredBody string, name string) bool { name + " != null", name + " == nullptr", name + " != nullptr", - "typeof " + name + " === ", - "typeof " + name + " == ", } if containsAny(loweredBody, guards) { return true @@ -356,7 +354,14 @@ func nullableUseLine(fn precisionFunction, name string) int { } func nullableStatementUsesOnlyNullSafeOperators(statement string, name string) bool { - return containsAny(statement, []string{name + "?.", name + "?.[", name + " ??"}) + if containsAny(statement, []string{name + "?.", name + "?.[", name + " ??"}) { + return true + } + // A typeof check only narrows the value on the branch controlled by that + // check. Do not treat an arbitrary typeof occurrence elsewhere in the + // function as a null guard. + quotedName := regexp.QuoteMeta(name) + return regexp.MustCompile(`typeof\s+` + quotedName + `\s*={2,3}\s*['"](?:string|number|boolean|bigint|symbol|function)['"]\s*\?[^:]*\b` + quotedName + `(?:\.|\[)`).MatchString(statement) } func firstUseLine(fn precisionFunction, name string) int { diff --git a/tests/checks/quality_precision_followup_retune_test.go b/tests/checks/quality_precision_followup_retune_test.go index dca7d5d..373e5cd 100644 --- a/tests/checks/quality_precision_followup_retune_test.go +++ b/tests/checks/quality_precision_followup_retune_test.go @@ -406,6 +406,26 @@ func TestDefensiveNullAssumptionCreditsTypeScriptNarrowing(t *testing.T) { assertCodeQualityRuleAbsentForPath(t, report, "defensive.null-assumption", "null-narrowing.ts:16") } +func TestDefensiveNullAssumptionRejectsUnsafeTypeofGuards(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "packages/api/src/lib/unsafe-typeof.ts"), strings.Join([]string{ + "export function objectCheck(user: User | null) {", + " if (typeof user === 'object') return user.email;", + "}", + "export function lateCheck(user: User | null) {", + " const email = user.email;", + " if (typeof user === 'string') return '';", + " return email;", + "}", + "interface User { email: string }", + }, "\n")) + + report := runQualityPrecisionScan(t, qualityPrecisionConfigForLanguage(dir, "typescript")) + + assertCodeQualityRulePresentForPathWithMessage(t, report, "defensive.null-assumption", "unsafe-typeof.ts:2") + assertCodeQualityRulePresentForPathWithMessage(t, report, "defensive.null-assumption", "unsafe-typeof.ts:5") +} + func TestDefensiveBoundaryInputSkipsTypedInternalDTOs(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "packages/api/src/routers/internal-dtos.ts"), strings.Join([]string{