Files
puaros/packages/guardian/src/infrastructure/strategies/MagicStringMatcher.ts
imfozilbek 8d400c9517 refactor: extract detector logic into focused strategy classes
Refactored three largest detectors to improve maintainability and reduce complexity:

- AggregateBoundaryDetector: 381 → 162 lines (57% reduction)
- HardcodeDetector: 459 → 89 lines (81% reduction)
- RepositoryPatternDetector: 479 → 106 lines (78% reduction)

Added 13 new strategy classes:
- FolderRegistry - centralized DDD folder name management
- AggregatePathAnalyzer - path parsing and aggregate extraction
- ImportValidator - import validation logic
- BraceTracker - brace and bracket counting
- ConstantsFileChecker - constants file detection
- ExportConstantAnalyzer - export const analysis
- MagicNumberMatcher - magic number detection
- MagicStringMatcher - magic string detection
- OrmTypeMatcher - ORM type matching
- MethodNameValidator - repository method validation
- RepositoryFileAnalyzer - file role detection
- RepositoryViolationDetector - violation detection logic

All 519 tests passing, zero ESLint errors, no breaking changes.
2025-11-25 17:41:32 +05:00

213 lines
5.8 KiB
TypeScript

import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
import { DETECTION_KEYWORDS } from "../constants/defaults"
import { HARDCODE_TYPES } from "../../shared/constants"
import { ExportConstantAnalyzer } from "./ExportConstantAnalyzer"
/**
* Detects magic strings in code
*
* Identifies hardcoded string values that should be extracted
* to constants, excluding test code, console logs, and type contexts.
*/
export class MagicStringMatcher {
private readonly stringRegex = /(['"`])(?:(?!\1).)+\1/g
private readonly allowedPatterns = [/^[a-z]$/i, /^\/$/, /^\\$/, /^\s+$/, /^,$/, /^\.$/]
private readonly typeContextPatterns = [
/^\s*type\s+\w+\s*=/i,
/^\s*interface\s+\w+/i,
/^\s*\w+\s*:\s*['"`]/,
/\s+as\s+['"`]/,
/Record<.*,\s*import\(/,
/typeof\s+\w+\s*===\s*['"`]/,
/['"`]\s*===\s*typeof\s+\w+/,
]
constructor(private readonly exportAnalyzer: ExportConstantAnalyzer) {}
/**
* Detects magic strings in code
*/
public detect(code: string): HardcodedValue[] {
const results: HardcodedValue[] = []
const lines = code.split("\n")
lines.forEach((line, lineIndex) => {
if (this.shouldSkipLine(line, lines, lineIndex)) {
return
}
this.detectStringsInLine(line, lineIndex, results)
})
return results
}
/**
* Checks if line should be skipped
*/
private shouldSkipLine(line: string, lines: string[], lineIndex: number): boolean {
if (
line.trim().startsWith("//") ||
line.trim().startsWith("*") ||
line.includes("import ") ||
line.includes("from ")
) {
return true
}
return this.exportAnalyzer.isInExportedConstant(lines, lineIndex)
}
/**
* Detects strings in a single line
*/
private detectStringsInLine(line: string, lineIndex: number, results: HardcodedValue[]): void {
let match
const regex = new RegExp(this.stringRegex)
while ((match = regex.exec(line)) !== null) {
const fullMatch = match[0]
const value = fullMatch.slice(1, -1)
if (this.shouldDetectString(fullMatch, value, line)) {
results.push(
HardcodedValue.create(
value,
HARDCODE_TYPES.MAGIC_STRING,
lineIndex + 1,
match.index,
line.trim(),
),
)
}
}
}
/**
* Checks if string should be detected
*/
private shouldDetectString(fullMatch: string, value: string, line: string): boolean {
if (fullMatch.startsWith("`") || value.includes("${")) {
return false
}
if (this.isAllowedString(value)) {
return false
}
return this.looksLikeMagicString(line, value)
}
/**
* Checks if string is allowed (short strings, single chars, etc.)
*/
private isAllowedString(str: string): boolean {
if (str.length <= 1) {
return true
}
return this.allowedPatterns.some((pattern) => pattern.test(str))
}
/**
* Checks if line context suggests a magic string
*/
private looksLikeMagicString(line: string, value: string): boolean {
const lowerLine = line.toLowerCase()
if (this.isTestCode(lowerLine)) {
return false
}
if (this.isConsoleLog(lowerLine)) {
return false
}
if (this.isInTypeContext(line)) {
return false
}
if (this.isInSymbolCall(line, value)) {
return false
}
if (this.isInImportCall(line, value)) {
return false
}
if (this.isUrlOrApi(value)) {
return true
}
if (/^\d{2,}$/.test(value)) {
return false
}
return value.length > 3
}
/**
* Checks if line is test code
*/
private isTestCode(lowerLine: string): boolean {
return (
lowerLine.includes(DETECTION_KEYWORDS.TEST) ||
lowerLine.includes(DETECTION_KEYWORDS.DESCRIBE)
)
}
/**
* Checks if line is console log
*/
private isConsoleLog(lowerLine: string): boolean {
return (
lowerLine.includes(DETECTION_KEYWORDS.CONSOLE_LOG) ||
lowerLine.includes(DETECTION_KEYWORDS.CONSOLE_ERROR)
)
}
/**
* Checks if line is in type context
*/
private isInTypeContext(line: string): boolean {
const trimmedLine = line.trim()
if (this.typeContextPatterns.some((pattern) => pattern.test(trimmedLine))) {
return true
}
if (trimmedLine.includes("|") && /['"`][^'"`]+['"`]\s*\|/.test(trimmedLine)) {
return true
}
return false
}
/**
* Checks if string is inside Symbol() call
*/
private isInSymbolCall(line: string, stringValue: string): boolean {
const symbolPattern = new RegExp(
`Symbol\\s*\\(\\s*['"\`]${stringValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\`]\\s*\\)`,
)
return symbolPattern.test(line)
}
/**
* Checks if string is inside import() call
*/
private isInImportCall(line: string, stringValue: string): boolean {
const importPattern = /import\s*\(\s*['"`][^'"`]+['"`]\s*\)/
return importPattern.test(line) && line.includes(stringValue)
}
/**
* Checks if string contains URL or API reference
*/
private isUrlOrApi(value: string): boolean {
return value.includes(DETECTION_KEYWORDS.HTTP) || value.includes(DETECTION_KEYWORDS.API)
}
}