driftdetect-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzers/ast-analyzer.d.ts +251 -0
- package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
- package/dist/analyzers/ast-analyzer.js +548 -0
- package/dist/analyzers/ast-analyzer.js.map +1 -0
- package/dist/analyzers/flow-analyzer.d.ts +241 -0
- package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
- package/dist/analyzers/flow-analyzer.js +1219 -0
- package/dist/analyzers/flow-analyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +18 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +19 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/semantic-analyzer.d.ts +252 -0
- package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
- package/dist/analyzers/semantic-analyzer.js +1182 -0
- package/dist/analyzers/semantic-analyzer.js.map +1 -0
- package/dist/analyzers/type-analyzer.d.ts +289 -0
- package/dist/analyzers/type-analyzer.d.ts.map +1 -0
- package/dist/analyzers/type-analyzer.js +1269 -0
- package/dist/analyzers/type-analyzer.js.map +1 -0
- package/dist/analyzers/types.d.ts +537 -0
- package/dist/analyzers/types.d.ts.map +1 -0
- package/dist/analyzers/types.js +11 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/config/config-loader.d.ts +166 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +429 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/config-validator.d.ts +204 -0
- package/dist/config/config-validator.d.ts.map +1 -0
- package/dist/config/config-validator.js +632 -0
- package/dist/config/config-validator.js.map +1 -0
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +26 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +47 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +7 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/exporter.d.ts +21 -0
- package/dist/manifest/exporter.d.ts.map +1 -0
- package/dist/manifest/exporter.js +339 -0
- package/dist/manifest/exporter.js.map +1 -0
- package/dist/manifest/index.d.ts +14 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +15 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest-store.d.ts +111 -0
- package/dist/manifest/manifest-store.d.ts.map +1 -0
- package/dist/manifest/manifest-store.js +418 -0
- package/dist/manifest/manifest-store.js.map +1 -0
- package/dist/manifest/types.d.ts +238 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +11 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/matcher/confidence-scorer.d.ts +188 -0
- package/dist/matcher/confidence-scorer.d.ts.map +1 -0
- package/dist/matcher/confidence-scorer.js +302 -0
- package/dist/matcher/confidence-scorer.js.map +1 -0
- package/dist/matcher/index.d.ts +24 -0
- package/dist/matcher/index.d.ts.map +1 -0
- package/dist/matcher/index.js +26 -0
- package/dist/matcher/index.js.map +1 -0
- package/dist/matcher/outlier-detector.d.ts +252 -0
- package/dist/matcher/outlier-detector.d.ts.map +1 -0
- package/dist/matcher/outlier-detector.js +544 -0
- package/dist/matcher/outlier-detector.js.map +1 -0
- package/dist/matcher/pattern-matcher.d.ts +169 -0
- package/dist/matcher/pattern-matcher.d.ts.map +1 -0
- package/dist/matcher/pattern-matcher.js +692 -0
- package/dist/matcher/pattern-matcher.js.map +1 -0
- package/dist/matcher/types.d.ts +476 -0
- package/dist/matcher/types.d.ts.map +1 -0
- package/dist/matcher/types.js +36 -0
- package/dist/matcher/types.js.map +1 -0
- package/dist/parsers/base-parser.d.ts +282 -0
- package/dist/parsers/base-parser.d.ts.map +1 -0
- package/dist/parsers/base-parser.js +421 -0
- package/dist/parsers/base-parser.js.map +1 -0
- package/dist/parsers/css-parser.d.ts +225 -0
- package/dist/parsers/css-parser.d.ts.map +1 -0
- package/dist/parsers/css-parser.js +477 -0
- package/dist/parsers/css-parser.js.map +1 -0
- package/dist/parsers/index.d.ts +15 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +15 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/json-parser.d.ts +219 -0
- package/dist/parsers/json-parser.d.ts.map +1 -0
- package/dist/parsers/json-parser.js +602 -0
- package/dist/parsers/json-parser.js.map +1 -0
- package/dist/parsers/markdown-parser.d.ts +276 -0
- package/dist/parsers/markdown-parser.d.ts.map +1 -0
- package/dist/parsers/markdown-parser.js +731 -0
- package/dist/parsers/markdown-parser.js.map +1 -0
- package/dist/parsers/parser-manager.d.ts +294 -0
- package/dist/parsers/parser-manager.d.ts.map +1 -0
- package/dist/parsers/parser-manager.js +738 -0
- package/dist/parsers/parser-manager.js.map +1 -0
- package/dist/parsers/python-parser.d.ts +204 -0
- package/dist/parsers/python-parser.d.ts.map +1 -0
- package/dist/parsers/python-parser.js +517 -0
- package/dist/parsers/python-parser.js.map +1 -0
- package/dist/parsers/types.d.ts +43 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +7 -0
- package/dist/parsers/types.js.map +1 -0
- package/dist/parsers/typescript-parser.d.ts +264 -0
- package/dist/parsers/typescript-parser.d.ts.map +1 -0
- package/dist/parsers/typescript-parser.js +658 -0
- package/dist/parsers/typescript-parser.js.map +1 -0
- package/dist/rules/evaluator.d.ts +305 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/evaluator.js +579 -0
- package/dist/rules/evaluator.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +13 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quick-fix-generator.d.ts +334 -0
- package/dist/rules/quick-fix-generator.d.ts.map +1 -0
- package/dist/rules/quick-fix-generator.js +1075 -0
- package/dist/rules/quick-fix-generator.js.map +1 -0
- package/dist/rules/rule-engine.d.ts +241 -0
- package/dist/rules/rule-engine.d.ts.map +1 -0
- package/dist/rules/rule-engine.js +585 -0
- package/dist/rules/rule-engine.js.map +1 -0
- package/dist/rules/severity-manager.d.ts +394 -0
- package/dist/rules/severity-manager.d.ts.map +1 -0
- package/dist/rules/severity-manager.js +619 -0
- package/dist/rules/severity-manager.js.map +1 -0
- package/dist/rules/types.d.ts +370 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +133 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/variant-manager.d.ts +388 -0
- package/dist/rules/variant-manager.d.ts.map +1 -0
- package/dist/rules/variant-manager.js +777 -0
- package/dist/rules/variant-manager.js.map +1 -0
- package/dist/scanner/change-detector.d.ts +164 -0
- package/dist/scanner/change-detector.d.ts.map +1 -0
- package/dist/scanner/change-detector.js +263 -0
- package/dist/scanner/change-detector.js.map +1 -0
- package/dist/scanner/dependency-graph.d.ts +270 -0
- package/dist/scanner/dependency-graph.d.ts.map +1 -0
- package/dist/scanner/dependency-graph.js +436 -0
- package/dist/scanner/dependency-graph.js.map +1 -0
- package/dist/scanner/file-walker.d.ts +127 -0
- package/dist/scanner/file-walker.d.ts.map +1 -0
- package/dist/scanner/file-walker.js +526 -0
- package/dist/scanner/file-walker.js.map +1 -0
- package/dist/scanner/index.d.ts +12 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +12 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/types.d.ts +218 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +10 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scanner/worker-pool.d.ts +317 -0
- package/dist/scanner/worker-pool.d.ts.map +1 -0
- package/dist/scanner/worker-pool.js +571 -0
- package/dist/scanner/worker-pool.js.map +1 -0
- package/dist/store/cache-manager.d.ts +179 -0
- package/dist/store/cache-manager.d.ts.map +1 -0
- package/dist/store/cache-manager.js +391 -0
- package/dist/store/cache-manager.js.map +1 -0
- package/dist/store/history-store.d.ts +314 -0
- package/dist/store/history-store.d.ts.map +1 -0
- package/dist/store/history-store.js +707 -0
- package/dist/store/history-store.js.map +1 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +26 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/lock-file-manager.d.ts +202 -0
- package/dist/store/lock-file-manager.d.ts.map +1 -0
- package/dist/store/lock-file-manager.js +475 -0
- package/dist/store/lock-file-manager.js.map +1 -0
- package/dist/store/pattern-store.d.ts +289 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +936 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/schema-validator.d.ts +159 -0
- package/dist/store/schema-validator.d.ts.map +1 -0
- package/dist/store/schema-validator.js +1096 -0
- package/dist/store/schema-validator.js.map +1 -0
- package/dist/store/types.d.ts +585 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +82 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types/analysis.d.ts +19 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +5 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/common.d.ts +7 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +5 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/patterns.d.ts +40 -0
- package/dist/types/patterns.d.ts.map +1 -0
- package/dist/types/patterns.js +7 -0
- package/dist/types/patterns.js.map +1 -0
- package/dist/types/violations.d.ts +7 -0
- package/dist/types/violations.d.ts.map +1 -0
- package/dist/types/violations.js +7 -0
- package/dist/types/violations.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matcher - Match code against pattern definitions
|
|
3
|
+
*
|
|
4
|
+
* Provides pattern matching capabilities using AST-based, regex-based,
|
|
5
|
+
* and structural matching methods. Supports caching of match results
|
|
6
|
+
* for improved performance.
|
|
7
|
+
*
|
|
8
|
+
* @requirements 5.1 - Pattern matching with confidence scoring
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* PatternMatcher class for matching code against pattern definitions.
|
|
12
|
+
*
|
|
13
|
+
* Supports multiple matching methods:
|
|
14
|
+
* - AST-based matching: Uses AST node types and properties
|
|
15
|
+
* - Regex-based matching: Uses regular expressions
|
|
16
|
+
* - Structural matching: Uses file paths and naming conventions
|
|
17
|
+
*
|
|
18
|
+
* @requirements 5.1 - Pattern matching with confidence scoring
|
|
19
|
+
*/
|
|
20
|
+
export class PatternMatcher {
|
|
21
|
+
config;
|
|
22
|
+
cache;
|
|
23
|
+
cacheMaxSize;
|
|
24
|
+
cacheTTL;
|
|
25
|
+
/**
|
|
26
|
+
* Create a new PatternMatcher instance.
|
|
27
|
+
*
|
|
28
|
+
* @param config - Matcher configuration options
|
|
29
|
+
*/
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.cache = new Map();
|
|
33
|
+
this.cacheMaxSize = config.cache?.maxSize ?? 1000;
|
|
34
|
+
this.cacheTTL = config.cache?.ttl ?? 60000; // 1 minute default
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Match code against a single pattern definition.
|
|
38
|
+
*
|
|
39
|
+
* @param context - The matcher context containing file info and AST
|
|
40
|
+
* @param pattern - The pattern definition to match against
|
|
41
|
+
* @param options - Optional matching options
|
|
42
|
+
* @returns Array of pattern match results
|
|
43
|
+
*/
|
|
44
|
+
match(context, pattern, options = {}) {
|
|
45
|
+
// Check if pattern is enabled
|
|
46
|
+
if (!pattern.enabled) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
// Check language filter
|
|
50
|
+
if (pattern.languages && pattern.languages.length > 0) {
|
|
51
|
+
if (!pattern.languages.includes(context.language)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Check file include/exclude patterns
|
|
56
|
+
if (!this.matchesFilePatterns(context.file, pattern)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
// Check cache if enabled
|
|
60
|
+
const useCache = options.useCache ?? this.config.cache?.enabled ?? true;
|
|
61
|
+
if (useCache) {
|
|
62
|
+
const cached = this.getCachedResults(context, pattern);
|
|
63
|
+
if (cached) {
|
|
64
|
+
return this.filterResults(cached, options);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Perform matching based on match type
|
|
68
|
+
let results;
|
|
69
|
+
try {
|
|
70
|
+
switch (pattern.matchType) {
|
|
71
|
+
case 'ast':
|
|
72
|
+
results = this.matchAST(context, pattern);
|
|
73
|
+
break;
|
|
74
|
+
case 'regex':
|
|
75
|
+
results = this.matchRegex(context, pattern);
|
|
76
|
+
break;
|
|
77
|
+
case 'structural':
|
|
78
|
+
results = this.matchStructural(context, pattern);
|
|
79
|
+
break;
|
|
80
|
+
case 'semantic':
|
|
81
|
+
// Semantic matching falls back to AST for now
|
|
82
|
+
results = this.matchAST(context, pattern);
|
|
83
|
+
break;
|
|
84
|
+
case 'custom':
|
|
85
|
+
// Custom matching not implemented yet
|
|
86
|
+
results = [];
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
results = [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// Handle errors gracefully
|
|
94
|
+
console.error(`Error matching pattern ${pattern.id}:`, error);
|
|
95
|
+
results = [];
|
|
96
|
+
}
|
|
97
|
+
// Cache results
|
|
98
|
+
if (useCache) {
|
|
99
|
+
this.cacheResults(context, pattern, results);
|
|
100
|
+
}
|
|
101
|
+
return this.filterResults(results, options);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Match code against multiple pattern definitions.
|
|
105
|
+
*
|
|
106
|
+
* @param context - The matcher context containing file info and AST
|
|
107
|
+
* @param patterns - Array of pattern definitions to match against
|
|
108
|
+
* @param options - Optional matching options
|
|
109
|
+
* @returns Matching result with all matches and errors
|
|
110
|
+
*/
|
|
111
|
+
matchAll(context, patterns, options = {}) {
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
const allMatches = [];
|
|
114
|
+
const errors = [];
|
|
115
|
+
for (const pattern of patterns) {
|
|
116
|
+
try {
|
|
117
|
+
const matches = this.match(context, pattern, options);
|
|
118
|
+
allMatches.push(...matches);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
errors.push({
|
|
122
|
+
message: error instanceof Error ? error.message : String(error),
|
|
123
|
+
patternId: pattern.id,
|
|
124
|
+
recoverable: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
file: context.file,
|
|
130
|
+
matches: allMatches,
|
|
131
|
+
outliers: [], // Outlier detection is handled by OutlierDetector
|
|
132
|
+
duration: Date.now() - startTime,
|
|
133
|
+
success: errors.length === 0,
|
|
134
|
+
errors,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Clear the match cache.
|
|
139
|
+
*/
|
|
140
|
+
clearCache() {
|
|
141
|
+
this.cache.clear();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get cache statistics.
|
|
145
|
+
*/
|
|
146
|
+
getCacheStats() {
|
|
147
|
+
return {
|
|
148
|
+
size: this.cache.size,
|
|
149
|
+
maxSize: this.cacheMaxSize,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// AST-based Matching
|
|
154
|
+
// ============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Perform AST-based pattern matching.
|
|
157
|
+
*/
|
|
158
|
+
matchAST(context, pattern) {
|
|
159
|
+
if (!context.ast) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const config = pattern.astConfig;
|
|
163
|
+
if (!config) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const results = [];
|
|
167
|
+
this.traverseAST(context.ast.rootNode, (node, depth) => {
|
|
168
|
+
// Check depth constraints
|
|
169
|
+
if (config.minDepth !== undefined && depth < config.minDepth) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (config.maxDepth !== undefined && depth > config.maxDepth) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const matchResult = this.matchASTNode(node, config);
|
|
176
|
+
if (matchResult.matches) {
|
|
177
|
+
const result = {
|
|
178
|
+
patternId: pattern.id,
|
|
179
|
+
location: this.nodeToLocation(node, context.file),
|
|
180
|
+
confidence: matchResult.confidence,
|
|
181
|
+
isOutlier: false,
|
|
182
|
+
matchType: 'ast',
|
|
183
|
+
timestamp: new Date(),
|
|
184
|
+
};
|
|
185
|
+
if (this.config.includeAstNodes) {
|
|
186
|
+
result.matchedNode = node;
|
|
187
|
+
}
|
|
188
|
+
if (this.config.includeMatchedText) {
|
|
189
|
+
result.matchedText = node.text;
|
|
190
|
+
}
|
|
191
|
+
results.push(result);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Match a single AST node against an AST match config.
|
|
198
|
+
*/
|
|
199
|
+
matchASTNode(node, config) {
|
|
200
|
+
let confidence = 1.0;
|
|
201
|
+
let matchCount = 0;
|
|
202
|
+
let totalChecks = 0;
|
|
203
|
+
// Check node type
|
|
204
|
+
if (config.nodeType) {
|
|
205
|
+
totalChecks++;
|
|
206
|
+
if (node.type === config.nodeType) {
|
|
207
|
+
matchCount++;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
return { matches: false, confidence: 0 };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Check properties
|
|
214
|
+
if (config.properties) {
|
|
215
|
+
for (const [key, value] of Object.entries(config.properties)) {
|
|
216
|
+
totalChecks++;
|
|
217
|
+
const nodeValue = node[key];
|
|
218
|
+
if (this.matchPropertyValue(nodeValue, value)) {
|
|
219
|
+
matchCount++;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
return { matches: false, confidence: 0 };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Check children patterns
|
|
227
|
+
if (config.children && config.children.length > 0) {
|
|
228
|
+
const childResults = this.matchASTChildren(node, config.children, config.matchDescendants);
|
|
229
|
+
if (!childResults.matches) {
|
|
230
|
+
return { matches: false, confidence: 0 };
|
|
231
|
+
}
|
|
232
|
+
confidence *= childResults.confidence;
|
|
233
|
+
}
|
|
234
|
+
// Calculate final confidence
|
|
235
|
+
if (totalChecks > 0) {
|
|
236
|
+
confidence *= matchCount / totalChecks;
|
|
237
|
+
}
|
|
238
|
+
return { matches: true, confidence };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Match AST children against child patterns.
|
|
242
|
+
*/
|
|
243
|
+
matchASTChildren(node, childPatterns, matchDescendants) {
|
|
244
|
+
const nodesToSearch = matchDescendants
|
|
245
|
+
? this.getAllDescendants(node)
|
|
246
|
+
: node.children;
|
|
247
|
+
let matchedCount = 0;
|
|
248
|
+
for (const childPattern of childPatterns) {
|
|
249
|
+
let found = false;
|
|
250
|
+
for (const child of nodesToSearch) {
|
|
251
|
+
const result = this.matchASTNode(child, childPattern);
|
|
252
|
+
if (result.matches) {
|
|
253
|
+
found = true;
|
|
254
|
+
matchedCount++;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!found) {
|
|
259
|
+
return { matches: false, confidence: 0 };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
matches: true,
|
|
264
|
+
confidence: matchedCount / childPatterns.length,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get all descendants of an AST node.
|
|
269
|
+
*/
|
|
270
|
+
getAllDescendants(node) {
|
|
271
|
+
const descendants = [];
|
|
272
|
+
const collect = (n) => {
|
|
273
|
+
for (const child of n.children) {
|
|
274
|
+
descendants.push(child);
|
|
275
|
+
collect(child);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
collect(node);
|
|
279
|
+
return descendants;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Traverse AST depth-first.
|
|
283
|
+
*/
|
|
284
|
+
traverseAST(node, visitor, depth = 0) {
|
|
285
|
+
visitor(node, depth);
|
|
286
|
+
for (const child of node.children) {
|
|
287
|
+
this.traverseAST(child, visitor, depth + 1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Match a property value against an expected value.
|
|
292
|
+
*/
|
|
293
|
+
matchPropertyValue(actual, expected) {
|
|
294
|
+
if (expected instanceof RegExp) {
|
|
295
|
+
return typeof actual === 'string' && expected.test(actual);
|
|
296
|
+
}
|
|
297
|
+
if (typeof expected === 'object' && expected !== null) {
|
|
298
|
+
if (typeof actual !== 'object' || actual === null) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
// Deep comparison for objects
|
|
302
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
303
|
+
}
|
|
304
|
+
return actual === expected;
|
|
305
|
+
}
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// Regex-based Matching
|
|
308
|
+
// ============================================================================
|
|
309
|
+
/**
|
|
310
|
+
* Perform regex-based pattern matching.
|
|
311
|
+
*/
|
|
312
|
+
matchRegex(context, pattern) {
|
|
313
|
+
const config = pattern.regexConfig;
|
|
314
|
+
if (!config) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
const results = [];
|
|
318
|
+
const regex = this.createRegex(config);
|
|
319
|
+
if (!regex) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const lines = context.content.split('\n');
|
|
323
|
+
let match;
|
|
324
|
+
// Reset regex state for global matching
|
|
325
|
+
regex.lastIndex = 0;
|
|
326
|
+
while ((match = regex.exec(context.content)) !== null) {
|
|
327
|
+
const location = this.indexToLocation(match.index, lines, context.file);
|
|
328
|
+
const captures = this.extractCaptures(match, config.captureGroups);
|
|
329
|
+
const result = {
|
|
330
|
+
patternId: pattern.id,
|
|
331
|
+
location,
|
|
332
|
+
confidence: 1.0, // Regex matches are binary
|
|
333
|
+
isOutlier: false,
|
|
334
|
+
matchType: 'regex',
|
|
335
|
+
timestamp: new Date(),
|
|
336
|
+
};
|
|
337
|
+
if (this.config.includeMatchedText) {
|
|
338
|
+
result.matchedText = match[0];
|
|
339
|
+
}
|
|
340
|
+
if (captures) {
|
|
341
|
+
result.captures = captures;
|
|
342
|
+
}
|
|
343
|
+
results.push(result);
|
|
344
|
+
// Prevent infinite loop for zero-length matches
|
|
345
|
+
if (match[0].length === 0) {
|
|
346
|
+
regex.lastIndex++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return results;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Create a RegExp from regex config.
|
|
353
|
+
*/
|
|
354
|
+
createRegex(config) {
|
|
355
|
+
try {
|
|
356
|
+
let flags = config.flags ?? 'g';
|
|
357
|
+
if (!flags.includes('g')) {
|
|
358
|
+
flags += 'g'; // Always use global flag for multiple matches
|
|
359
|
+
}
|
|
360
|
+
if (config.multiline && !flags.includes('m')) {
|
|
361
|
+
flags += 'm';
|
|
362
|
+
}
|
|
363
|
+
return new RegExp(config.pattern, flags);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Extract named captures from a regex match.
|
|
371
|
+
*/
|
|
372
|
+
extractCaptures(match, captureGroups) {
|
|
373
|
+
if (!captureGroups || captureGroups.length === 0) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
const captures = {};
|
|
377
|
+
for (let i = 0; i < captureGroups.length; i++) {
|
|
378
|
+
const groupName = captureGroups[i];
|
|
379
|
+
const groupValue = match[i + 1]; // Groups start at index 1
|
|
380
|
+
if (groupName && groupValue !== undefined) {
|
|
381
|
+
captures[groupName] = groupValue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return Object.keys(captures).length > 0 ? captures : undefined;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Convert a string index to a location.
|
|
388
|
+
*/
|
|
389
|
+
indexToLocation(index, lines, file) {
|
|
390
|
+
let currentIndex = 0;
|
|
391
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
392
|
+
const line = lines[lineNum];
|
|
393
|
+
if (line === undefined)
|
|
394
|
+
continue;
|
|
395
|
+
const lineLength = line.length + 1; // +1 for newline
|
|
396
|
+
if (currentIndex + lineLength > index) {
|
|
397
|
+
return {
|
|
398
|
+
file,
|
|
399
|
+
line: lineNum + 1, // 1-indexed
|
|
400
|
+
column: index - currentIndex + 1, // 1-indexed
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
currentIndex += lineLength;
|
|
404
|
+
}
|
|
405
|
+
// Default to end of file
|
|
406
|
+
return {
|
|
407
|
+
file,
|
|
408
|
+
line: lines.length,
|
|
409
|
+
column: 1,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Structural Matching
|
|
414
|
+
// ============================================================================
|
|
415
|
+
/**
|
|
416
|
+
* Perform structural pattern matching.
|
|
417
|
+
*/
|
|
418
|
+
matchStructural(context, pattern) {
|
|
419
|
+
const config = pattern.structuralConfig;
|
|
420
|
+
if (!config) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
const results = [];
|
|
424
|
+
let confidence = 1.0;
|
|
425
|
+
let matchCount = 0;
|
|
426
|
+
let totalChecks = 0;
|
|
427
|
+
// Check path pattern
|
|
428
|
+
if (config.pathPattern) {
|
|
429
|
+
totalChecks++;
|
|
430
|
+
if (this.matchGlobPattern(context.file, config.pathPattern)) {
|
|
431
|
+
matchCount++;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
return []; // Path pattern is required to match
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Check directory pattern
|
|
438
|
+
if (config.directoryPattern) {
|
|
439
|
+
totalChecks++;
|
|
440
|
+
const dir = this.getDirectory(context.file);
|
|
441
|
+
if (this.matchGlobPattern(dir, config.directoryPattern)) {
|
|
442
|
+
matchCount++;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Check naming pattern
|
|
449
|
+
if (config.namingPattern) {
|
|
450
|
+
totalChecks++;
|
|
451
|
+
const fileName = this.getFileName(context.file);
|
|
452
|
+
if (this.matchNamingPattern(fileName, config.namingPattern)) {
|
|
453
|
+
matchCount++;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Check extension
|
|
460
|
+
if (config.extension) {
|
|
461
|
+
totalChecks++;
|
|
462
|
+
const ext = this.getExtension(context.file);
|
|
463
|
+
if (ext === config.extension || ext === `.${config.extension}`) {
|
|
464
|
+
matchCount++;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Calculate confidence
|
|
471
|
+
if (totalChecks > 0) {
|
|
472
|
+
confidence = matchCount / totalChecks;
|
|
473
|
+
}
|
|
474
|
+
// If all checks passed, add a match for the file
|
|
475
|
+
if (matchCount === totalChecks && totalChecks > 0) {
|
|
476
|
+
results.push({
|
|
477
|
+
patternId: pattern.id,
|
|
478
|
+
location: {
|
|
479
|
+
file: context.file,
|
|
480
|
+
line: 1,
|
|
481
|
+
column: 1,
|
|
482
|
+
},
|
|
483
|
+
confidence,
|
|
484
|
+
isOutlier: false,
|
|
485
|
+
matchType: 'structural',
|
|
486
|
+
timestamp: new Date(),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return results;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Match a path against a glob pattern.
|
|
493
|
+
*/
|
|
494
|
+
matchGlobPattern(path, pattern) {
|
|
495
|
+
// Convert glob pattern to regex
|
|
496
|
+
const regexPattern = pattern
|
|
497
|
+
.replace(/\./g, '\\.')
|
|
498
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
499
|
+
.replace(/\*/g, '[^/]*')
|
|
500
|
+
.replace(/\?/g, '[^/]')
|
|
501
|
+
.replace(/{{GLOBSTAR}}/g, '.*');
|
|
502
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
503
|
+
return regex.test(path);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Match a filename against a naming pattern.
|
|
507
|
+
*/
|
|
508
|
+
matchNamingPattern(fileName, pattern) {
|
|
509
|
+
// Support common naming conventions
|
|
510
|
+
switch (pattern.toLowerCase()) {
|
|
511
|
+
case 'pascalcase':
|
|
512
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(fileName);
|
|
513
|
+
case 'camelcase':
|
|
514
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(fileName);
|
|
515
|
+
case 'kebab-case':
|
|
516
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(fileName);
|
|
517
|
+
case 'snake_case':
|
|
518
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(fileName);
|
|
519
|
+
case 'screaming_snake_case':
|
|
520
|
+
return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(fileName);
|
|
521
|
+
default:
|
|
522
|
+
// Treat as regex pattern
|
|
523
|
+
try {
|
|
524
|
+
const regex = new RegExp(pattern);
|
|
525
|
+
return regex.test(fileName);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get the directory part of a path.
|
|
534
|
+
*/
|
|
535
|
+
getDirectory(path) {
|
|
536
|
+
const lastSlash = path.lastIndexOf('/');
|
|
537
|
+
return lastSlash >= 0 ? path.substring(0, lastSlash) : '';
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get the filename without extension.
|
|
541
|
+
*/
|
|
542
|
+
getFileName(path) {
|
|
543
|
+
const lastSlash = path.lastIndexOf('/');
|
|
544
|
+
const fileName = lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
|
545
|
+
const lastDot = fileName.lastIndexOf('.');
|
|
546
|
+
return lastDot >= 0 ? fileName.substring(0, lastDot) : fileName;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get the file extension.
|
|
550
|
+
*/
|
|
551
|
+
getExtension(path) {
|
|
552
|
+
const lastDot = path.lastIndexOf('.');
|
|
553
|
+
return lastDot >= 0 ? path.substring(lastDot) : '';
|
|
554
|
+
}
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// File Pattern Matching
|
|
557
|
+
// ============================================================================
|
|
558
|
+
/**
|
|
559
|
+
* Check if a file matches the pattern's include/exclude patterns.
|
|
560
|
+
*/
|
|
561
|
+
matchesFilePatterns(file, pattern) {
|
|
562
|
+
// Check exclude patterns first
|
|
563
|
+
if (pattern.excludePatterns && pattern.excludePatterns.length > 0) {
|
|
564
|
+
for (const excludePattern of pattern.excludePatterns) {
|
|
565
|
+
if (this.matchGlobPattern(file, excludePattern)) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Check include patterns
|
|
571
|
+
if (pattern.includePatterns && pattern.includePatterns.length > 0) {
|
|
572
|
+
for (const includePattern of pattern.includePatterns) {
|
|
573
|
+
if (this.matchGlobPattern(file, includePattern)) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return false; // No include pattern matched
|
|
578
|
+
}
|
|
579
|
+
return true; // No include patterns means include all
|
|
580
|
+
}
|
|
581
|
+
// ============================================================================
|
|
582
|
+
// Caching
|
|
583
|
+
// ============================================================================
|
|
584
|
+
/**
|
|
585
|
+
* Generate a cache key for a context and pattern.
|
|
586
|
+
*/
|
|
587
|
+
getCacheKey(context, pattern) {
|
|
588
|
+
return `${context.file}:${pattern.id}`;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Generate a content hash for cache validation.
|
|
592
|
+
*/
|
|
593
|
+
getContentHash(content) {
|
|
594
|
+
// Simple hash function for cache validation
|
|
595
|
+
let hash = 0;
|
|
596
|
+
for (let i = 0; i < content.length; i++) {
|
|
597
|
+
const char = content.charCodeAt(i);
|
|
598
|
+
hash = ((hash << 5) - hash) + char;
|
|
599
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
600
|
+
}
|
|
601
|
+
return hash.toString(16);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Get cached results if available and valid.
|
|
605
|
+
*/
|
|
606
|
+
getCachedResults(context, pattern) {
|
|
607
|
+
const key = this.getCacheKey(context, pattern);
|
|
608
|
+
const entry = this.cache.get(key);
|
|
609
|
+
if (!entry) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
// Check TTL
|
|
613
|
+
if (Date.now() - entry.timestamp > this.cacheTTL) {
|
|
614
|
+
this.cache.delete(key);
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
// Check content hash
|
|
618
|
+
const currentHash = this.getContentHash(context.content);
|
|
619
|
+
if (entry.contentHash !== currentHash) {
|
|
620
|
+
this.cache.delete(key);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
return entry.results;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Cache match results.
|
|
627
|
+
*/
|
|
628
|
+
cacheResults(context, pattern, results) {
|
|
629
|
+
// Evict old entries if cache is full
|
|
630
|
+
if (this.cache.size >= this.cacheMaxSize) {
|
|
631
|
+
this.evictOldestEntry();
|
|
632
|
+
}
|
|
633
|
+
const key = this.getCacheKey(context, pattern);
|
|
634
|
+
this.cache.set(key, {
|
|
635
|
+
results,
|
|
636
|
+
timestamp: Date.now(),
|
|
637
|
+
contentHash: this.getContentHash(context.content),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Evict the oldest cache entry.
|
|
642
|
+
*/
|
|
643
|
+
evictOldestEntry() {
|
|
644
|
+
let oldestKey = null;
|
|
645
|
+
let oldestTime = Infinity;
|
|
646
|
+
for (const [key, entry] of this.cache) {
|
|
647
|
+
if (entry.timestamp < oldestTime) {
|
|
648
|
+
oldestTime = entry.timestamp;
|
|
649
|
+
oldestKey = key;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (oldestKey) {
|
|
653
|
+
this.cache.delete(oldestKey);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// ============================================================================
|
|
657
|
+
// Result Filtering
|
|
658
|
+
// ============================================================================
|
|
659
|
+
/**
|
|
660
|
+
* Filter results based on options.
|
|
661
|
+
*/
|
|
662
|
+
filterResults(results, options) {
|
|
663
|
+
let filtered = results;
|
|
664
|
+
// Filter by minimum confidence
|
|
665
|
+
const minConfidence = options.minConfidence ?? this.config.minConfidence ?? 0;
|
|
666
|
+
if (minConfidence > 0) {
|
|
667
|
+
filtered = filtered.filter((r) => r.confidence >= minConfidence);
|
|
668
|
+
}
|
|
669
|
+
// Limit results
|
|
670
|
+
const maxMatches = options.maxMatches ?? this.config.maxMatchesPerPattern;
|
|
671
|
+
if (maxMatches !== undefined && filtered.length > maxMatches) {
|
|
672
|
+
filtered = filtered.slice(0, maxMatches);
|
|
673
|
+
}
|
|
674
|
+
return filtered;
|
|
675
|
+
}
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// Utility Methods
|
|
678
|
+
// ============================================================================
|
|
679
|
+
/**
|
|
680
|
+
* Convert an AST node to a Location.
|
|
681
|
+
*/
|
|
682
|
+
nodeToLocation(node, file) {
|
|
683
|
+
return {
|
|
684
|
+
file,
|
|
685
|
+
line: node.startPosition.row + 1, // Convert to 1-indexed
|
|
686
|
+
column: node.startPosition.column + 1, // Convert to 1-indexed
|
|
687
|
+
endLine: node.endPosition.row + 1,
|
|
688
|
+
endColumn: node.endPosition.column + 1,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
//# sourceMappingURL=pattern-matcher.js.map
|