diff --git a/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts b/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts index 4eec5c4..30f599b 100644 --- a/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts +++ b/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts @@ -26,6 +26,19 @@ export class HardcodeDetector implements IHardcodeDetector { private readonly ALLOWED_STRING_PATTERNS = [/^[a-z]$/i, /^\/$/, /^\\$/, /^\s+$/, /^,$/, /^\.$/] + /** + * Patterns to detect TypeScript type contexts where strings should be ignored + */ + private readonly TYPE_CONTEXT_PATTERNS = [ + /^\s*type\s+\w+\s*=/i, // type Foo = ... + /^\s*interface\s+\w+/i, // interface Foo { ... } + /^\s*\w+\s*:\s*['"`]/, // property: 'value' (in type or interface) + /\s+as\s+['"`]/, // ... as 'type' + /Record<.*,\s*import\(/, // Record with import type + /typeof\s+\w+\s*===\s*['"`]/, // typeof x === 'string' + /['"`]\s*===\s*typeof\s+\w+/, // 'string' === typeof x + ] + /** * Detects all hardcoded values (both numbers and strings) in the given code * @@ -43,14 +56,15 @@ export class HardcodeDetector implements IHardcodeDetector { } /** - * Check if a file is a constants definition file + * Check if a file is a constants definition file or DI tokens file */ private isConstantsFile(filePath: string): boolean { const _fileName = filePath.split("/").pop() ?? "" const constantsPatterns = [ /^constants?\.(ts|js)$/i, /constants?\/.*\.(ts|js)$/i, - /\/(constants|config|settings|defaults)\.ts$/i, + /\/(constants|config|settings|defaults|tokens)\.ts$/i, + /\/di\/tokens\.(ts|js)$/i, ] return constantsPatterns.some((pattern) => pattern.test(filePath)) } @@ -341,6 +355,18 @@ export class HardcodeDetector implements IHardcodeDetector { return false } + if (this.isInTypeContext(line)) { + return false + } + + if (this.isInSymbolCall(line, value)) { + return false + } + + if (this.isInImportCall(line, value)) { + return false + } + if (value.includes(DETECTION_KEYWORDS.HTTP) || value.includes(DETECTION_KEYWORDS.API)) { return true } @@ -388,4 +414,46 @@ export class HardcodeDetector implements IHardcodeDetector { const end = Math.min(line.length, index + 30) return line.substring(start, end) } + + /** + * Check if a line is in a TypeScript type definition context + * Examples: + * - type Foo = 'a' | 'b' + * - interface Bar { prop: 'value' } + * - Record + * - ... as 'type' + */ + private isInTypeContext(line: string): boolean { + const trimmedLine = line.trim() + + if (this.TYPE_CONTEXT_PATTERNS.some((pattern) => pattern.test(trimmedLine))) { + return true + } + + if (trimmedLine.includes("|") && /['"`][^'"`]+['"`]\s*\|/.test(trimmedLine)) { + return true + } + + return false + } + + /** + * Check if a string is inside a Symbol() call + * Example: Symbol('TOKEN_NAME') + */ + private isInSymbolCall(line: string, stringValue: string): boolean { + const symbolPattern = new RegExp( + `Symbol\\s*\\(\\s*['"\`]${stringValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\`]\\s*\\)`, + ) + return symbolPattern.test(line) + } + + /** + * Check if a string is inside an import() call + * Example: import('../../path/to/module.js') + */ + private isInImportCall(line: string, stringValue: string): boolean { + const importPattern = /import\s*\(\s*['"`][^'"`]+['"`]\s*\)/ + return importPattern.test(line) && line.includes(stringValue) + } } diff --git a/packages/guardian/tests/unit/infrastructure/HardcodeDetector.test.ts b/packages/guardian/tests/unit/infrastructure/HardcodeDetector.test.ts index d387f29..0ea2944 100644 --- a/packages/guardian/tests/unit/infrastructure/HardcodeDetector.test.ts +++ b/packages/guardian/tests/unit/infrastructure/HardcodeDetector.test.ts @@ -468,4 +468,102 @@ const b = 2` expect(result[0].context).toContain("5000") }) }) + + describe("TypeScript type contexts (false positive reduction)", () => { + it("should NOT detect strings in union types", () => { + const code = `type Status = 'active' | 'inactive' | 'pending'` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in interface property types", () => { + const code = `interface Config { mode: 'development' | 'production' }` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in type aliases", () => { + const code = `type Theme = 'light' | 'dark'` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in type assertions", () => { + const code = `const mode = getMode() as 'read' | 'write'` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in Symbol() calls", () => { + const code = `const TOKEN = Symbol('MY_TOKEN')` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in multiple Symbol() calls", () => { + const code = ` + export const LOGGER = Symbol('LOGGER') + export const DATABASE = Symbol('DATABASE') + export const CACHE = Symbol('CACHE') + ` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in import() calls", () => { + const code = `const module = import('../../path/to/module.js')` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in typeof checks", () => { + const code = `if (typeof x === 'string') { }` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should NOT detect strings in reverse typeof checks", () => { + const code = `if ('number' === typeof count) { }` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result).toHaveLength(0) + }) + + it("should skip tokens.ts files completely", () => { + const code = ` + export const LOGGER = Symbol('LOGGER') + export const DATABASE = Symbol('DATABASE') + const url = "http://localhost:8080" + ` + const result = detector.detectAll(code, "src/di/tokens.ts") + + expect(result).toHaveLength(0) + }) + + it("should skip tokens.js files completely", () => { + const code = `const TOKEN = Symbol('TOKEN')` + const result = detector.detectAll(code, "src/di/tokens.js") + + expect(result).toHaveLength(0) + }) + + it("should detect real magic strings even with type contexts nearby", () => { + const code = ` + type Mode = 'read' | 'write' + const apiKey = "secret-key-12345" + ` + const result = detector.detectMagicStrings(code, "test.ts") + + expect(result.length).toBeGreaterThan(0) + expect(result.some((r) => r.value === "secret-key-12345")).toBe(true) + }) + }) })