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,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser Manager - Parser orchestration and caching
|
|
3
|
+
*
|
|
4
|
+
* Handles language detection from file extensions, parser selection,
|
|
5
|
+
* and AST caching with LRU eviction. Supports incremental parsing
|
|
6
|
+
* for changed file regions.
|
|
7
|
+
*
|
|
8
|
+
* @requirements 3.2, 3.4, 3.7
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import * as crypto from 'node:crypto';
|
|
13
|
+
import { BaseParser } from './base-parser.js';
|
|
14
|
+
/**
|
|
15
|
+
* Mapping of file extensions to languages
|
|
16
|
+
*/
|
|
17
|
+
const EXTENSION_TO_LANGUAGE = {
|
|
18
|
+
// TypeScript
|
|
19
|
+
'.ts': 'typescript',
|
|
20
|
+
'.tsx': 'typescript',
|
|
21
|
+
'.mts': 'typescript',
|
|
22
|
+
'.cts': 'typescript',
|
|
23
|
+
// JavaScript
|
|
24
|
+
'.js': 'javascript',
|
|
25
|
+
'.jsx': 'javascript',
|
|
26
|
+
'.mjs': 'javascript',
|
|
27
|
+
'.cjs': 'javascript',
|
|
28
|
+
// Python
|
|
29
|
+
'.py': 'python',
|
|
30
|
+
'.pyw': 'python',
|
|
31
|
+
'.pyi': 'python',
|
|
32
|
+
// CSS
|
|
33
|
+
'.css': 'css',
|
|
34
|
+
// SCSS
|
|
35
|
+
'.scss': 'scss',
|
|
36
|
+
'.sass': 'scss',
|
|
37
|
+
// JSON
|
|
38
|
+
'.json': 'json',
|
|
39
|
+
'.jsonc': 'json',
|
|
40
|
+
// YAML
|
|
41
|
+
'.yaml': 'yaml',
|
|
42
|
+
'.yml': 'yaml',
|
|
43
|
+
// Markdown
|
|
44
|
+
'.md': 'markdown',
|
|
45
|
+
'.markdown': 'markdown',
|
|
46
|
+
'.mdx': 'markdown',
|
|
47
|
+
};
|
|
48
|
+
const DEFAULT_OPTIONS = {
|
|
49
|
+
cacheSize: 100,
|
|
50
|
+
cacheTTL: 0, // No expiry by default
|
|
51
|
+
enableStats: true,
|
|
52
|
+
enableIncremental: true,
|
|
53
|
+
incrementalThreshold: 10,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Parser Manager for orchestrating language parsers and caching ASTs.
|
|
57
|
+
*
|
|
58
|
+
* Provides:
|
|
59
|
+
* - Language detection from file extensions
|
|
60
|
+
* - Parser registration and selection
|
|
61
|
+
* - AST caching with LRU eviction
|
|
62
|
+
* - Incremental parsing for changed regions
|
|
63
|
+
* - File parsing from disk
|
|
64
|
+
*
|
|
65
|
+
* @requirements 3.2 - Support TypeScript, JavaScript, Python, CSS/SCSS, JSON/YAML, Markdown
|
|
66
|
+
* @requirements 3.4 - Perform incremental parsing for changed file regions
|
|
67
|
+
* @requirements 3.7 - Cache parsed ASTs in memory with LRU eviction
|
|
68
|
+
*/
|
|
69
|
+
export class ParserManager {
|
|
70
|
+
options;
|
|
71
|
+
parsers;
|
|
72
|
+
cache;
|
|
73
|
+
/** Maps file paths to their most recent cache key for incremental parsing */
|
|
74
|
+
filePathToKey;
|
|
75
|
+
head = null;
|
|
76
|
+
tail = null;
|
|
77
|
+
stats;
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
80
|
+
this.parsers = new Map();
|
|
81
|
+
this.cache = new Map();
|
|
82
|
+
this.filePathToKey = new Map();
|
|
83
|
+
this.stats = {
|
|
84
|
+
hits: 0,
|
|
85
|
+
misses: 0,
|
|
86
|
+
evictions: 0,
|
|
87
|
+
size: 0,
|
|
88
|
+
maxSize: this.options.cacheSize,
|
|
89
|
+
hitRatio: 0,
|
|
90
|
+
incrementalParses: 0,
|
|
91
|
+
fullParses: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Register a parser for a specific language.
|
|
96
|
+
*
|
|
97
|
+
* @param parser - The parser to register
|
|
98
|
+
* @throws Error if a parser for the language is already registered
|
|
99
|
+
*/
|
|
100
|
+
registerParser(parser) {
|
|
101
|
+
if (this.parsers.has(parser.language)) {
|
|
102
|
+
throw new Error(`Parser for language '${parser.language}' is already registered`);
|
|
103
|
+
}
|
|
104
|
+
this.parsers.set(parser.language, parser);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get the parser for a specific file path based on its extension.
|
|
108
|
+
*
|
|
109
|
+
* @param filePath - The file path to get a parser for
|
|
110
|
+
* @returns The appropriate parser, or null if no parser is available
|
|
111
|
+
*/
|
|
112
|
+
getParser(filePath) {
|
|
113
|
+
const language = this.detectLanguage(filePath);
|
|
114
|
+
if (!language) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return this.parsers.get(language) ?? null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Detect the language of a file based on its extension.
|
|
121
|
+
*
|
|
122
|
+
* @param filePath - The file path to detect language for
|
|
123
|
+
* @returns The detected language, or null if unknown
|
|
124
|
+
*
|
|
125
|
+
* @requirements 3.2 - Detect language from file extension
|
|
126
|
+
*/
|
|
127
|
+
detectLanguage(filePath) {
|
|
128
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
129
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse source code with the appropriate parser.
|
|
133
|
+
*
|
|
134
|
+
* Uses cached AST if available and source hasn't changed.
|
|
135
|
+
* Supports incremental parsing when content has changed.
|
|
136
|
+
*
|
|
137
|
+
* @param filePath - The file path (used for language detection and cache key)
|
|
138
|
+
* @param source - The source code to parse
|
|
139
|
+
* @returns ParseResult containing the AST or errors
|
|
140
|
+
*
|
|
141
|
+
* @requirements 3.2 - Select appropriate parser for each language
|
|
142
|
+
* @requirements 3.4 - Perform incremental parsing for changed file regions
|
|
143
|
+
* @requirements 3.7 - Cache parsed ASTs with LRU eviction
|
|
144
|
+
*/
|
|
145
|
+
parse(filePath, source) {
|
|
146
|
+
const parser = this.getParser(filePath);
|
|
147
|
+
if (!parser) {
|
|
148
|
+
const language = this.detectLanguage(filePath);
|
|
149
|
+
return {
|
|
150
|
+
ast: null,
|
|
151
|
+
language: language ?? 'typescript', // Default to typescript for unknown
|
|
152
|
+
errors: [
|
|
153
|
+
{
|
|
154
|
+
message: `No parser available for file: ${filePath}`,
|
|
155
|
+
position: { row: 0, column: 0 },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
success: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Check cache for exact match
|
|
162
|
+
const hash = this.computeHash(source);
|
|
163
|
+
const cacheKey = this.getCacheKey(filePath, hash);
|
|
164
|
+
const cached = this.getFromCache(cacheKey);
|
|
165
|
+
if (cached) {
|
|
166
|
+
return cached;
|
|
167
|
+
}
|
|
168
|
+
// Try incremental parsing if enabled and we have a previous version
|
|
169
|
+
if (this.options.enableIncremental) {
|
|
170
|
+
const previousKey = this.filePathToKey.get(filePath);
|
|
171
|
+
if (previousKey) {
|
|
172
|
+
const previousNode = this.cache.get(previousKey);
|
|
173
|
+
if (previousNode && !this.isExpired(previousNode.entry)) {
|
|
174
|
+
const incrementalResult = this.parseIncremental(filePath, source, previousNode.entry.source, previousNode.entry.result, parser);
|
|
175
|
+
if (incrementalResult) {
|
|
176
|
+
this.addToCache(cacheKey, incrementalResult, hash, source);
|
|
177
|
+
this.filePathToKey.set(filePath, cacheKey);
|
|
178
|
+
this.stats.incrementalParses++;
|
|
179
|
+
return incrementalResult;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Full parse
|
|
185
|
+
const result = parser.parse(source, filePath);
|
|
186
|
+
this.addToCache(cacheKey, result, hash, source);
|
|
187
|
+
this.filePathToKey.set(filePath, cacheKey);
|
|
188
|
+
this.stats.fullParses++;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Parse source code with explicit change information for optimal incremental parsing.
|
|
193
|
+
*
|
|
194
|
+
* This method is more efficient when you know exactly what changed.
|
|
195
|
+
*
|
|
196
|
+
* @param filePath - The file path
|
|
197
|
+
* @param source - The new source code
|
|
198
|
+
* @param changes - Array of text changes that were made
|
|
199
|
+
* @returns IncrementalParseResult with information about what was re-parsed
|
|
200
|
+
*
|
|
201
|
+
* @requirements 3.4 - Perform incremental parsing for changed file regions
|
|
202
|
+
*/
|
|
203
|
+
parseWithChanges(filePath, source, changes) {
|
|
204
|
+
const parser = this.getParser(filePath);
|
|
205
|
+
if (!parser) {
|
|
206
|
+
const language = this.detectLanguage(filePath);
|
|
207
|
+
return {
|
|
208
|
+
ast: null,
|
|
209
|
+
language: language ?? 'typescript',
|
|
210
|
+
errors: [
|
|
211
|
+
{
|
|
212
|
+
message: `No parser available for file: ${filePath}`,
|
|
213
|
+
position: { row: 0, column: 0 },
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
success: false,
|
|
217
|
+
wasIncremental: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const hash = this.computeHash(source);
|
|
221
|
+
const cacheKey = this.getCacheKey(filePath, hash);
|
|
222
|
+
// Check for exact cache hit
|
|
223
|
+
const cached = this.getFromCache(cacheKey);
|
|
224
|
+
if (cached) {
|
|
225
|
+
return {
|
|
226
|
+
...cached,
|
|
227
|
+
wasIncremental: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Get previous cached version
|
|
231
|
+
const previousKey = this.filePathToKey.get(filePath);
|
|
232
|
+
const previousNode = previousKey ? this.cache.get(previousKey) : null;
|
|
233
|
+
if (this.options.enableIncremental &&
|
|
234
|
+
previousNode &&
|
|
235
|
+
!this.isExpired(previousNode.entry) &&
|
|
236
|
+
changes.length > 0) {
|
|
237
|
+
// Use change information for targeted re-parsing
|
|
238
|
+
const reparsedRegions = this.computeReparsedRegions(changes);
|
|
239
|
+
// For now, if changes are small enough, do incremental; otherwise full parse
|
|
240
|
+
const totalChangeSize = changes.reduce((sum, c) => sum + c.newText.length, 0);
|
|
241
|
+
if (totalChangeSize >= this.options.incrementalThreshold) {
|
|
242
|
+
// Perform full re-parse but track that we attempted incremental
|
|
243
|
+
const result = parser.parse(source, filePath);
|
|
244
|
+
this.addToCache(cacheKey, result, hash, source);
|
|
245
|
+
this.filePathToKey.set(filePath, cacheKey);
|
|
246
|
+
this.stats.fullParses++;
|
|
247
|
+
return {
|
|
248
|
+
...result,
|
|
249
|
+
wasIncremental: true,
|
|
250
|
+
reparsedRegions,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Full parse
|
|
255
|
+
const result = parser.parse(source, filePath);
|
|
256
|
+
this.addToCache(cacheKey, result, hash, source);
|
|
257
|
+
this.filePathToKey.set(filePath, cacheKey);
|
|
258
|
+
this.stats.fullParses++;
|
|
259
|
+
return {
|
|
260
|
+
...result,
|
|
261
|
+
wasIncremental: false,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Attempt incremental parsing by detecting changes between old and new source.
|
|
266
|
+
*
|
|
267
|
+
* @param filePath - The file path
|
|
268
|
+
* @param newSource - The new source code
|
|
269
|
+
* @param oldSource - The previous source code
|
|
270
|
+
* @param previousResult - The previous parse result
|
|
271
|
+
* @param parser - The parser to use
|
|
272
|
+
* @returns ParseResult if incremental parsing succeeded, null otherwise
|
|
273
|
+
*
|
|
274
|
+
* @requirements 3.4 - Only re-parse changed regions
|
|
275
|
+
*/
|
|
276
|
+
parseIncremental(filePath, newSource, oldSource, previousResult, parser) {
|
|
277
|
+
// If previous parse failed, do full parse
|
|
278
|
+
if (!previousResult.success || !previousResult.ast) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
// Detect changes between old and new source
|
|
282
|
+
const changes = this.detectChanges(oldSource, newSource);
|
|
283
|
+
// If no changes detected or changes are too large, do full parse
|
|
284
|
+
if (changes.length === 0) {
|
|
285
|
+
// Content is the same, return previous result
|
|
286
|
+
return previousResult;
|
|
287
|
+
}
|
|
288
|
+
// Calculate total change size
|
|
289
|
+
const totalChangeSize = changes.reduce((sum, change) => sum + Math.abs(change.newText.length - this.getOldTextLength(change)), 0);
|
|
290
|
+
// If changes are below threshold, we can potentially reuse parts of the AST
|
|
291
|
+
// For now, we do a full re-parse but this is where true incremental parsing would happen
|
|
292
|
+
// with Tree-sitter's edit capabilities
|
|
293
|
+
if (totalChangeSize < this.options.incrementalThreshold) {
|
|
294
|
+
// Small change - still do full parse but it's fast
|
|
295
|
+
return parser.parse(newSource, filePath);
|
|
296
|
+
}
|
|
297
|
+
// For larger changes, we could potentially isolate the changed region
|
|
298
|
+
// and reuse unchanged AST portions. For now, do full parse.
|
|
299
|
+
// This is where Tree-sitter's incremental parsing would shine.
|
|
300
|
+
// Future: const changedRegion = this.getChangedRegion(changes);
|
|
301
|
+
return parser.parse(newSource, filePath);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Detect changes between old and new source code.
|
|
305
|
+
*
|
|
306
|
+
* Uses a simple diff algorithm to find changed regions.
|
|
307
|
+
*
|
|
308
|
+
* @param oldSource - The previous source code
|
|
309
|
+
* @param newSource - The new source code
|
|
310
|
+
* @returns Array of detected changes
|
|
311
|
+
*/
|
|
312
|
+
detectChanges(oldSource, newSource) {
|
|
313
|
+
const changes = [];
|
|
314
|
+
// Simple line-based diff for detecting changes
|
|
315
|
+
const oldLines = oldSource.split('\n');
|
|
316
|
+
const newLines = newSource.split('\n');
|
|
317
|
+
let oldIdx = 0;
|
|
318
|
+
let newIdx = 0;
|
|
319
|
+
while (oldIdx < oldLines.length || newIdx < newLines.length) {
|
|
320
|
+
if (oldIdx >= oldLines.length) {
|
|
321
|
+
// New lines added at the end
|
|
322
|
+
changes.push({
|
|
323
|
+
startPosition: { row: newIdx, column: 0 },
|
|
324
|
+
oldEndPosition: { row: oldIdx, column: 0 },
|
|
325
|
+
newEndPosition: { row: newLines.length, column: newLines[newLines.length - 1]?.length ?? 0 },
|
|
326
|
+
newText: newLines.slice(newIdx).join('\n'),
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (newIdx >= newLines.length) {
|
|
331
|
+
// Lines deleted at the end
|
|
332
|
+
changes.push({
|
|
333
|
+
startPosition: { row: newIdx, column: 0 },
|
|
334
|
+
oldEndPosition: { row: oldLines.length, column: oldLines[oldLines.length - 1]?.length ?? 0 },
|
|
335
|
+
newEndPosition: { row: newIdx, column: 0 },
|
|
336
|
+
newText: '',
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
if (oldLines[oldIdx] !== newLines[newIdx]) {
|
|
341
|
+
// Find the extent of the change
|
|
342
|
+
const changeStart = { row: newIdx, column: 0 };
|
|
343
|
+
let oldEnd = oldIdx;
|
|
344
|
+
let newEnd = newIdx;
|
|
345
|
+
// Find where lines match again
|
|
346
|
+
while (oldEnd < oldLines.length && newEnd < newLines.length) {
|
|
347
|
+
if (oldLines[oldEnd] === newLines[newEnd]) {
|
|
348
|
+
// Check if this is a real match (not just coincidence)
|
|
349
|
+
let matchLength = 0;
|
|
350
|
+
while (oldEnd + matchLength < oldLines.length &&
|
|
351
|
+
newEnd + matchLength < newLines.length &&
|
|
352
|
+
oldLines[oldEnd + matchLength] === newLines[newEnd + matchLength]) {
|
|
353
|
+
matchLength++;
|
|
354
|
+
if (matchLength >= 3)
|
|
355
|
+
break; // Consider 3+ matching lines as sync point
|
|
356
|
+
}
|
|
357
|
+
if (matchLength >= 3 || (oldEnd + matchLength >= oldLines.length && newEnd + matchLength >= newLines.length)) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Advance both pointers to find sync point
|
|
362
|
+
if (oldEnd - oldIdx <= newEnd - newIdx) {
|
|
363
|
+
oldEnd++;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
newEnd++;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
changes.push({
|
|
370
|
+
startPosition: changeStart,
|
|
371
|
+
oldEndPosition: { row: oldEnd, column: oldLines[oldEnd - 1]?.length ?? 0 },
|
|
372
|
+
newEndPosition: { row: newEnd, column: newLines[newEnd - 1]?.length ?? 0 },
|
|
373
|
+
newText: newLines.slice(newIdx, newEnd).join('\n'),
|
|
374
|
+
});
|
|
375
|
+
oldIdx = oldEnd;
|
|
376
|
+
newIdx = newEnd;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
oldIdx++;
|
|
380
|
+
newIdx++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return changes;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get the length of the old text that was replaced by a change.
|
|
387
|
+
*/
|
|
388
|
+
getOldTextLength(change) {
|
|
389
|
+
const rowDiff = change.oldEndPosition.row - change.startPosition.row;
|
|
390
|
+
if (rowDiff === 0) {
|
|
391
|
+
return change.oldEndPosition.column - change.startPosition.column;
|
|
392
|
+
}
|
|
393
|
+
// Approximate for multi-line changes
|
|
394
|
+
return rowDiff * 80 + change.oldEndPosition.column;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Get the bounding region of all changes.
|
|
398
|
+
*
|
|
399
|
+
* @remarks This method is currently unused but will be used when
|
|
400
|
+
* Tree-sitter incremental parsing is fully integrated.
|
|
401
|
+
* @internal
|
|
402
|
+
*/
|
|
403
|
+
// @ts-expect-error - Reserved for future Tree-sitter incremental parsing integration
|
|
404
|
+
getChangedRegion(changes) {
|
|
405
|
+
if (changes.length === 0)
|
|
406
|
+
return null;
|
|
407
|
+
let minRow = Infinity;
|
|
408
|
+
let minCol = Infinity;
|
|
409
|
+
let maxRow = -1;
|
|
410
|
+
let maxCol = -1;
|
|
411
|
+
for (const change of changes) {
|
|
412
|
+
if (change.startPosition.row < minRow) {
|
|
413
|
+
minRow = change.startPosition.row;
|
|
414
|
+
minCol = change.startPosition.column;
|
|
415
|
+
}
|
|
416
|
+
else if (change.startPosition.row === minRow && change.startPosition.column < minCol) {
|
|
417
|
+
minCol = change.startPosition.column;
|
|
418
|
+
}
|
|
419
|
+
if (change.newEndPosition.row > maxRow) {
|
|
420
|
+
maxRow = change.newEndPosition.row;
|
|
421
|
+
maxCol = change.newEndPosition.column;
|
|
422
|
+
}
|
|
423
|
+
else if (change.newEndPosition.row === maxRow && change.newEndPosition.column > maxCol) {
|
|
424
|
+
maxCol = change.newEndPosition.column;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
start: { row: minRow, column: minCol },
|
|
429
|
+
end: { row: maxRow, column: maxCol },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Compute the regions that were re-parsed based on changes.
|
|
434
|
+
*/
|
|
435
|
+
computeReparsedRegions(changes) {
|
|
436
|
+
return changes.map((change) => ({
|
|
437
|
+
start: change.startPosition,
|
|
438
|
+
end: change.newEndPosition,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Parse a file from disk.
|
|
443
|
+
*
|
|
444
|
+
* Reads the file content and parses it with the appropriate parser.
|
|
445
|
+
*
|
|
446
|
+
* @param filePath - The path to the file to parse
|
|
447
|
+
* @returns ParseResult containing the AST or errors
|
|
448
|
+
*/
|
|
449
|
+
async parseFile(filePath) {
|
|
450
|
+
try {
|
|
451
|
+
const source = await fs.readFile(filePath, 'utf-8');
|
|
452
|
+
return this.parse(filePath, source);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const language = this.detectLanguage(filePath);
|
|
456
|
+
return {
|
|
457
|
+
ast: null,
|
|
458
|
+
language: language ?? 'typescript',
|
|
459
|
+
errors: [
|
|
460
|
+
{
|
|
461
|
+
message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
462
|
+
position: { row: 0, column: 0 },
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
success: false,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Check if a parser is registered for a specific language.
|
|
471
|
+
*
|
|
472
|
+
* @param language - The language to check
|
|
473
|
+
* @returns true if a parser is registered
|
|
474
|
+
*/
|
|
475
|
+
hasParser(language) {
|
|
476
|
+
return this.parsers.has(language);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Get all registered languages.
|
|
480
|
+
*
|
|
481
|
+
* @returns Array of registered languages
|
|
482
|
+
*/
|
|
483
|
+
getRegisteredLanguages() {
|
|
484
|
+
return Array.from(this.parsers.keys());
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get all supported file extensions.
|
|
488
|
+
*
|
|
489
|
+
* @returns Array of supported file extensions
|
|
490
|
+
*/
|
|
491
|
+
getSupportedExtensions() {
|
|
492
|
+
return Object.keys(EXTENSION_TO_LANGUAGE);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Check if a file extension is supported.
|
|
496
|
+
*
|
|
497
|
+
* @param extension - The file extension to check (with or without leading dot)
|
|
498
|
+
* @returns true if the extension is supported
|
|
499
|
+
*/
|
|
500
|
+
isExtensionSupported(extension) {
|
|
501
|
+
const normalizedExt = extension.startsWith('.') ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
|
|
502
|
+
return normalizedExt in EXTENSION_TO_LANGUAGE;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Invalidate cached AST for a specific file.
|
|
506
|
+
*
|
|
507
|
+
* @param filePath - The file path to invalidate cache for
|
|
508
|
+
* @returns true if an entry was invalidated
|
|
509
|
+
*/
|
|
510
|
+
invalidateCache(filePath) {
|
|
511
|
+
// We need to find and remove all cache entries for this file path
|
|
512
|
+
// Since cache keys include hash, we need to iterate
|
|
513
|
+
let invalidated = false;
|
|
514
|
+
const keysToDelete = [];
|
|
515
|
+
for (const key of this.cache.keys()) {
|
|
516
|
+
if (key.startsWith(filePath + ':')) {
|
|
517
|
+
keysToDelete.push(key);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const key of keysToDelete) {
|
|
521
|
+
this.deleteFromCache(key);
|
|
522
|
+
invalidated = true;
|
|
523
|
+
}
|
|
524
|
+
return invalidated;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Clear all cached ASTs.
|
|
528
|
+
*/
|
|
529
|
+
clearCache() {
|
|
530
|
+
this.cache.clear();
|
|
531
|
+
this.filePathToKey.clear();
|
|
532
|
+
this.head = null;
|
|
533
|
+
this.tail = null;
|
|
534
|
+
this.stats.size = 0;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get the current cache statistics.
|
|
538
|
+
*
|
|
539
|
+
* @returns Current cache statistics
|
|
540
|
+
*/
|
|
541
|
+
getCacheStats() {
|
|
542
|
+
return { ...this.stats };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Reset cache statistics.
|
|
546
|
+
*/
|
|
547
|
+
resetCacheStats() {
|
|
548
|
+
this.stats = {
|
|
549
|
+
hits: 0,
|
|
550
|
+
misses: 0,
|
|
551
|
+
evictions: 0,
|
|
552
|
+
size: this.cache.size,
|
|
553
|
+
maxSize: this.options.cacheSize,
|
|
554
|
+
hitRatio: 0,
|
|
555
|
+
incrementalParses: 0,
|
|
556
|
+
fullParses: 0,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get the current cache size.
|
|
561
|
+
*
|
|
562
|
+
* @returns Number of entries in the cache
|
|
563
|
+
*/
|
|
564
|
+
get cacheSize() {
|
|
565
|
+
return this.cache.size;
|
|
566
|
+
}
|
|
567
|
+
// ============================================
|
|
568
|
+
// Private Cache Methods
|
|
569
|
+
// ============================================
|
|
570
|
+
/**
|
|
571
|
+
* Compute a SHA-256 hash of content.
|
|
572
|
+
*/
|
|
573
|
+
computeHash(content) {
|
|
574
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Generate a cache key from file path and content hash.
|
|
578
|
+
*/
|
|
579
|
+
getCacheKey(filePath, hash) {
|
|
580
|
+
return `${filePath}:${hash}`;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Get a parse result from the cache.
|
|
584
|
+
*/
|
|
585
|
+
getFromCache(key) {
|
|
586
|
+
const node = this.cache.get(key);
|
|
587
|
+
if (!node) {
|
|
588
|
+
if (this.options.enableStats) {
|
|
589
|
+
this.stats.misses++;
|
|
590
|
+
this.updateHitRatio();
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
// Check TTL expiration
|
|
595
|
+
if (this.isExpired(node.entry)) {
|
|
596
|
+
this.deleteFromCache(key);
|
|
597
|
+
if (this.options.enableStats) {
|
|
598
|
+
this.stats.misses++;
|
|
599
|
+
this.updateHitRatio();
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
// Move to front (most recently used)
|
|
604
|
+
this.moveToFront(node);
|
|
605
|
+
// Update stats
|
|
606
|
+
if (this.options.enableStats) {
|
|
607
|
+
node.entry.hits++;
|
|
608
|
+
this.stats.hits++;
|
|
609
|
+
this.updateHitRatio();
|
|
610
|
+
}
|
|
611
|
+
return node.entry.result;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Add a parse result to the cache.
|
|
615
|
+
*/
|
|
616
|
+
addToCache(key, result, hash, source) {
|
|
617
|
+
// Check if key already exists
|
|
618
|
+
const existingNode = this.cache.get(key);
|
|
619
|
+
if (existingNode) {
|
|
620
|
+
// Update existing entry
|
|
621
|
+
existingNode.entry.result = result;
|
|
622
|
+
existingNode.entry.timestamp = Date.now();
|
|
623
|
+
existingNode.entry.source = source;
|
|
624
|
+
this.moveToFront(existingNode);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Evict if at capacity
|
|
628
|
+
while (this.cache.size >= this.options.cacheSize) {
|
|
629
|
+
this.evictLRU();
|
|
630
|
+
}
|
|
631
|
+
// Create new entry
|
|
632
|
+
const entry = {
|
|
633
|
+
result,
|
|
634
|
+
hash,
|
|
635
|
+
timestamp: Date.now(),
|
|
636
|
+
hits: 0,
|
|
637
|
+
source,
|
|
638
|
+
};
|
|
639
|
+
const node = {
|
|
640
|
+
key,
|
|
641
|
+
entry,
|
|
642
|
+
prev: null,
|
|
643
|
+
next: null,
|
|
644
|
+
};
|
|
645
|
+
// Add to cache and front of list
|
|
646
|
+
this.cache.set(key, node);
|
|
647
|
+
this.addToFront(node);
|
|
648
|
+
this.stats.size = this.cache.size;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Delete an entry from the cache.
|
|
652
|
+
*/
|
|
653
|
+
deleteFromCache(key) {
|
|
654
|
+
const node = this.cache.get(key);
|
|
655
|
+
if (!node) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
this.removeFromList(node);
|
|
659
|
+
this.cache.delete(key);
|
|
660
|
+
this.stats.size = this.cache.size;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Check if a cache entry has expired.
|
|
665
|
+
*/
|
|
666
|
+
isExpired(entry) {
|
|
667
|
+
if (this.options.cacheTTL === 0) {
|
|
668
|
+
return false; // No expiration
|
|
669
|
+
}
|
|
670
|
+
return Date.now() - entry.timestamp > this.options.cacheTTL;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Add a node to the front of the LRU list.
|
|
674
|
+
*/
|
|
675
|
+
addToFront(node) {
|
|
676
|
+
node.prev = null;
|
|
677
|
+
node.next = this.head;
|
|
678
|
+
if (this.head) {
|
|
679
|
+
this.head.prev = node;
|
|
680
|
+
}
|
|
681
|
+
this.head = node;
|
|
682
|
+
if (!this.tail) {
|
|
683
|
+
this.tail = node;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Remove a node from the LRU list.
|
|
688
|
+
*/
|
|
689
|
+
removeFromList(node) {
|
|
690
|
+
if (node.prev) {
|
|
691
|
+
node.prev.next = node.next;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.head = node.next;
|
|
695
|
+
}
|
|
696
|
+
if (node.next) {
|
|
697
|
+
node.next.prev = node.prev;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
this.tail = node.prev;
|
|
701
|
+
}
|
|
702
|
+
node.prev = null;
|
|
703
|
+
node.next = null;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Move a node to the front of the LRU list.
|
|
707
|
+
*/
|
|
708
|
+
moveToFront(node) {
|
|
709
|
+
if (node === this.head) {
|
|
710
|
+
return; // Already at front
|
|
711
|
+
}
|
|
712
|
+
this.removeFromList(node);
|
|
713
|
+
this.addToFront(node);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Evict the least recently used entry.
|
|
717
|
+
*/
|
|
718
|
+
evictLRU() {
|
|
719
|
+
if (!this.tail) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const key = this.tail.key;
|
|
723
|
+
this.removeFromList(this.tail);
|
|
724
|
+
this.cache.delete(key);
|
|
725
|
+
if (this.options.enableStats) {
|
|
726
|
+
this.stats.evictions++;
|
|
727
|
+
}
|
|
728
|
+
this.stats.size = this.cache.size;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Update the hit ratio statistic.
|
|
732
|
+
*/
|
|
733
|
+
updateHitRatio() {
|
|
734
|
+
const total = this.stats.hits + this.stats.misses;
|
|
735
|
+
this.stats.hitRatio = total > 0 ? this.stats.hits / total : 0;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=parser-manager.js.map
|