refactor: migrate hardcode detector from regex to AST-based analysis

- Replace regex-based matchers with tree-sitter AST traversal
- Add duplicate value tracking across files
- Implement boolean literal detection
- Add value type classification (email, url, ip, api_key, etc.)
- Improve context awareness with AST node analysis
- Reduce false positives with better constant detection

Breaking changes removed:
- BraceTracker.ts
- ExportConstantAnalyzer.ts
- MagicNumberMatcher.ts
- MagicStringMatcher.ts

New components added:
- AstTreeTraverser for AST walking
- DuplicateValueTracker for cross-file tracking
- AstContextChecker for node context analysis
- AstNumberAnalyzer, AstStringAnalyzer, AstBooleanAnalyzer
- ValuePatternMatcher for type detection

Test coverage: 87.97% statements, 96.75% functions
This commit is contained in:
imfozilbek
2025-11-26 17:38:30 +05:00
parent 656571860e
commit af094eb54a
24 changed files with 2641 additions and 648 deletions

View File

@@ -0,0 +1,144 @@
import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
import { CONFIG_KEYWORDS, DETECTION_VALUES, HARDCODE_TYPES } from "../../shared/constants/rules"
import { AstContextChecker } from "./AstContextChecker"
import { ValuePatternMatcher } from "./ValuePatternMatcher"
/**
* AST-based analyzer for detecting magic strings
*
* Analyzes string literal nodes in the AST to determine if they are
* hardcoded values that should be extracted to constants.
*
* Detects various types of hardcoded strings:
* - URLs and connection strings
* - Email addresses
* - IP addresses
* - File paths
* - Dates
* - API keys
*/
export class AstStringAnalyzer {
private readonly patternMatcher: ValuePatternMatcher
constructor(private readonly contextChecker: AstContextChecker) {
this.patternMatcher = new ValuePatternMatcher()
}
/**
* Analyzes a string node and returns a violation if it's a magic string
*/
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null {
const stringFragment = node.children.find((child) => child.type === "string_fragment")
if (!stringFragment) {
return null
}
const value = stringFragment.text
if (value.length <= 3) {
return null
}
if (this.contextChecker.isInExportedConstant(node)) {
return null
}
if (this.contextChecker.isInTypeContext(node)) {
return null
}
if (this.contextChecker.isInImportStatement(node)) {
return null
}
if (this.contextChecker.isInTestDescription(node)) {
return null
}
if (this.contextChecker.isInConsoleCall(node)) {
return null
}
if (this.contextChecker.isInSymbolCall(node)) {
return null
}
if (this.contextChecker.isInTypeofCheck(node)) {
return null
}
if (this.shouldDetect(node, value)) {
return this.createViolation(node, value, lines)
}
return null
}
/**
* Checks if string value should be detected
*/
private shouldDetect(node: Parser.SyntaxNode, value: string): boolean {
if (this.patternMatcher.shouldDetect(value)) {
return true
}
if (this.hasConfigurationContext(node)) {
return true
}
return false
}
/**
* Checks if string is in a configuration-related context
*/
private hasConfigurationContext(node: Parser.SyntaxNode): boolean {
const context = this.contextChecker.getNodeContext(node).toLowerCase()
const configKeywords = [
"url",
"uri",
...CONFIG_KEYWORDS.NETWORK,
"api",
...CONFIG_KEYWORDS.DATABASE,
"db",
"env",
...CONFIG_KEYWORDS.SECURITY,
"key",
...CONFIG_KEYWORDS.MESSAGES,
"label",
]
return configKeywords.some((keyword) => context.includes(keyword))
}
/**
* Creates a HardcodedValue violation from a string node
*/
private createViolation(
node: Parser.SyntaxNode,
value: string,
lines: string[],
): HardcodedValue {
const lineNumber = node.startPosition.row + 1
const column = node.startPosition.column
const context = lines[node.startPosition.row]?.trim() ?? ""
const detectedType = this.patternMatcher.detectType(value)
const valueType =
detectedType ||
(this.hasConfigurationContext(node)
? DETECTION_VALUES.TYPE_CONFIG
: DETECTION_VALUES.TYPE_GENERIC)
return HardcodedValue.create(
value,
HARDCODE_TYPES.MAGIC_STRING as HardcodeType,
lineNumber,
column,
context,
valueType,
)
}
}