Files
puaros/packages/guardian/src/infrastructure/analyzers/AstTreeTraverser.ts
imfozilbek af094eb54a 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
2025-11-26 17:38:30 +05:00

105 lines
3.6 KiB
TypeScript

import Parser from "tree-sitter"
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
import { AstBooleanAnalyzer } from "../strategies/AstBooleanAnalyzer"
import { AstConfigObjectAnalyzer } from "../strategies/AstConfigObjectAnalyzer"
import { AstNumberAnalyzer } from "../strategies/AstNumberAnalyzer"
import { AstStringAnalyzer } from "../strategies/AstStringAnalyzer"
/**
* AST tree traverser for detecting hardcoded values
*
* Walks through the Abstract Syntax Tree and uses analyzers
* to detect hardcoded numbers, strings, booleans, and configuration objects.
* Also tracks value usage to identify "almost constants" - values used 2+ times.
*/
export class AstTreeTraverser {
constructor(
private readonly numberAnalyzer: AstNumberAnalyzer,
private readonly stringAnalyzer: AstStringAnalyzer,
private readonly booleanAnalyzer: AstBooleanAnalyzer,
private readonly configObjectAnalyzer: AstConfigObjectAnalyzer,
) {}
/**
* Traverses the AST tree and collects hardcoded values
*/
public traverse(tree: Parser.Tree, sourceCode: string): HardcodedValue[] {
const results: HardcodedValue[] = []
const lines = sourceCode.split("\n")
const cursor = tree.walk()
this.visit(cursor, lines, results)
this.markAlmostConstants(results)
return results
}
/**
* Marks values that appear multiple times in the same file
*/
private markAlmostConstants(results: HardcodedValue[]): void {
const valueUsage = new Map<string, number>()
for (const result of results) {
const key = `${result.type}:${String(result.value)}`
valueUsage.set(key, (valueUsage.get(key) || 0) + 1)
}
for (let i = 0; i < results.length; i++) {
const result = results[i]
const key = `${result.type}:${String(result.value)}`
const count = valueUsage.get(key) || 0
if (count >= 2 && !result.withinFileUsageCount) {
results[i] = HardcodedValue.create(
result.value,
result.type,
result.line,
result.column,
result.context,
result.valueType,
result.duplicateLocations,
count,
)
}
}
}
/**
* Recursively visits AST nodes
*/
private visit(cursor: Parser.TreeCursor, lines: string[], results: HardcodedValue[]): void {
const node = cursor.currentNode
if (node.type === "object") {
const violation = this.configObjectAnalyzer.analyze(node, lines)
if (violation) {
results.push(violation)
}
} else if (node.type === "number") {
const violation = this.numberAnalyzer.analyze(node, lines)
if (violation) {
results.push(violation)
}
} else if (node.type === "string") {
const violation = this.stringAnalyzer.analyze(node, lines)
if (violation) {
results.push(violation)
}
} else if (node.type === "true" || node.type === "false") {
const violation = this.booleanAnalyzer.analyze(node, lines)
if (violation) {
results.push(violation)
}
}
if (cursor.gotoFirstChild()) {
do {
this.visit(cursor, lines, results)
} while (cursor.gotoNextSibling())
cursor.gotoParent()
}
}
}