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.
Files changed (221) hide show
  1. package/dist/analyzers/ast-analyzer.d.ts +251 -0
  2. package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
  3. package/dist/analyzers/ast-analyzer.js +548 -0
  4. package/dist/analyzers/ast-analyzer.js.map +1 -0
  5. package/dist/analyzers/flow-analyzer.d.ts +241 -0
  6. package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
  7. package/dist/analyzers/flow-analyzer.js +1219 -0
  8. package/dist/analyzers/flow-analyzer.js.map +1 -0
  9. package/dist/analyzers/index.d.ts +18 -0
  10. package/dist/analyzers/index.d.ts.map +1 -0
  11. package/dist/analyzers/index.js +19 -0
  12. package/dist/analyzers/index.js.map +1 -0
  13. package/dist/analyzers/semantic-analyzer.d.ts +252 -0
  14. package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
  15. package/dist/analyzers/semantic-analyzer.js +1182 -0
  16. package/dist/analyzers/semantic-analyzer.js.map +1 -0
  17. package/dist/analyzers/type-analyzer.d.ts +289 -0
  18. package/dist/analyzers/type-analyzer.d.ts.map +1 -0
  19. package/dist/analyzers/type-analyzer.js +1269 -0
  20. package/dist/analyzers/type-analyzer.js.map +1 -0
  21. package/dist/analyzers/types.d.ts +537 -0
  22. package/dist/analyzers/types.d.ts.map +1 -0
  23. package/dist/analyzers/types.js +11 -0
  24. package/dist/analyzers/types.js.map +1 -0
  25. package/dist/config/config-loader.d.ts +166 -0
  26. package/dist/config/config-loader.d.ts.map +1 -0
  27. package/dist/config/config-loader.js +429 -0
  28. package/dist/config/config-loader.js.map +1 -0
  29. package/dist/config/config-validator.d.ts +204 -0
  30. package/dist/config/config-validator.d.ts.map +1 -0
  31. package/dist/config/config-validator.js +632 -0
  32. package/dist/config/config-validator.js.map +1 -0
  33. package/dist/config/defaults.d.ts +8 -0
  34. package/dist/config/defaults.d.ts.map +1 -0
  35. package/dist/config/defaults.js +26 -0
  36. package/dist/config/defaults.js.map +1 -0
  37. package/dist/config/index.d.ts +10 -0
  38. package/dist/config/index.d.ts.map +1 -0
  39. package/dist/config/index.js +10 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/config/types.d.ts +47 -0
  42. package/dist/config/types.d.ts.map +1 -0
  43. package/dist/config/types.js +7 -0
  44. package/dist/config/types.js.map +1 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/manifest/exporter.d.ts +21 -0
  50. package/dist/manifest/exporter.d.ts.map +1 -0
  51. package/dist/manifest/exporter.js +339 -0
  52. package/dist/manifest/exporter.js.map +1 -0
  53. package/dist/manifest/index.d.ts +14 -0
  54. package/dist/manifest/index.d.ts.map +1 -0
  55. package/dist/manifest/index.js +15 -0
  56. package/dist/manifest/index.js.map +1 -0
  57. package/dist/manifest/manifest-store.d.ts +111 -0
  58. package/dist/manifest/manifest-store.d.ts.map +1 -0
  59. package/dist/manifest/manifest-store.js +418 -0
  60. package/dist/manifest/manifest-store.js.map +1 -0
  61. package/dist/manifest/types.d.ts +238 -0
  62. package/dist/manifest/types.d.ts.map +1 -0
  63. package/dist/manifest/types.js +11 -0
  64. package/dist/manifest/types.js.map +1 -0
  65. package/dist/matcher/confidence-scorer.d.ts +188 -0
  66. package/dist/matcher/confidence-scorer.d.ts.map +1 -0
  67. package/dist/matcher/confidence-scorer.js +302 -0
  68. package/dist/matcher/confidence-scorer.js.map +1 -0
  69. package/dist/matcher/index.d.ts +24 -0
  70. package/dist/matcher/index.d.ts.map +1 -0
  71. package/dist/matcher/index.js +26 -0
  72. package/dist/matcher/index.js.map +1 -0
  73. package/dist/matcher/outlier-detector.d.ts +252 -0
  74. package/dist/matcher/outlier-detector.d.ts.map +1 -0
  75. package/dist/matcher/outlier-detector.js +544 -0
  76. package/dist/matcher/outlier-detector.js.map +1 -0
  77. package/dist/matcher/pattern-matcher.d.ts +169 -0
  78. package/dist/matcher/pattern-matcher.d.ts.map +1 -0
  79. package/dist/matcher/pattern-matcher.js +692 -0
  80. package/dist/matcher/pattern-matcher.js.map +1 -0
  81. package/dist/matcher/types.d.ts +476 -0
  82. package/dist/matcher/types.d.ts.map +1 -0
  83. package/dist/matcher/types.js +36 -0
  84. package/dist/matcher/types.js.map +1 -0
  85. package/dist/parsers/base-parser.d.ts +282 -0
  86. package/dist/parsers/base-parser.d.ts.map +1 -0
  87. package/dist/parsers/base-parser.js +421 -0
  88. package/dist/parsers/base-parser.js.map +1 -0
  89. package/dist/parsers/css-parser.d.ts +225 -0
  90. package/dist/parsers/css-parser.d.ts.map +1 -0
  91. package/dist/parsers/css-parser.js +477 -0
  92. package/dist/parsers/css-parser.js.map +1 -0
  93. package/dist/parsers/index.d.ts +15 -0
  94. package/dist/parsers/index.d.ts.map +1 -0
  95. package/dist/parsers/index.js +15 -0
  96. package/dist/parsers/index.js.map +1 -0
  97. package/dist/parsers/json-parser.d.ts +219 -0
  98. package/dist/parsers/json-parser.d.ts.map +1 -0
  99. package/dist/parsers/json-parser.js +602 -0
  100. package/dist/parsers/json-parser.js.map +1 -0
  101. package/dist/parsers/markdown-parser.d.ts +276 -0
  102. package/dist/parsers/markdown-parser.d.ts.map +1 -0
  103. package/dist/parsers/markdown-parser.js +731 -0
  104. package/dist/parsers/markdown-parser.js.map +1 -0
  105. package/dist/parsers/parser-manager.d.ts +294 -0
  106. package/dist/parsers/parser-manager.d.ts.map +1 -0
  107. package/dist/parsers/parser-manager.js +738 -0
  108. package/dist/parsers/parser-manager.js.map +1 -0
  109. package/dist/parsers/python-parser.d.ts +204 -0
  110. package/dist/parsers/python-parser.d.ts.map +1 -0
  111. package/dist/parsers/python-parser.js +517 -0
  112. package/dist/parsers/python-parser.js.map +1 -0
  113. package/dist/parsers/types.d.ts +43 -0
  114. package/dist/parsers/types.d.ts.map +1 -0
  115. package/dist/parsers/types.js +7 -0
  116. package/dist/parsers/types.js.map +1 -0
  117. package/dist/parsers/typescript-parser.d.ts +264 -0
  118. package/dist/parsers/typescript-parser.d.ts.map +1 -0
  119. package/dist/parsers/typescript-parser.js +658 -0
  120. package/dist/parsers/typescript-parser.js.map +1 -0
  121. package/dist/rules/evaluator.d.ts +305 -0
  122. package/dist/rules/evaluator.d.ts.map +1 -0
  123. package/dist/rules/evaluator.js +579 -0
  124. package/dist/rules/evaluator.js.map +1 -0
  125. package/dist/rules/index.d.ts +13 -0
  126. package/dist/rules/index.d.ts.map +1 -0
  127. package/dist/rules/index.js +13 -0
  128. package/dist/rules/index.js.map +1 -0
  129. package/dist/rules/quick-fix-generator.d.ts +334 -0
  130. package/dist/rules/quick-fix-generator.d.ts.map +1 -0
  131. package/dist/rules/quick-fix-generator.js +1075 -0
  132. package/dist/rules/quick-fix-generator.js.map +1 -0
  133. package/dist/rules/rule-engine.d.ts +241 -0
  134. package/dist/rules/rule-engine.d.ts.map +1 -0
  135. package/dist/rules/rule-engine.js +585 -0
  136. package/dist/rules/rule-engine.js.map +1 -0
  137. package/dist/rules/severity-manager.d.ts +394 -0
  138. package/dist/rules/severity-manager.d.ts.map +1 -0
  139. package/dist/rules/severity-manager.js +619 -0
  140. package/dist/rules/severity-manager.js.map +1 -0
  141. package/dist/rules/types.d.ts +370 -0
  142. package/dist/rules/types.d.ts.map +1 -0
  143. package/dist/rules/types.js +133 -0
  144. package/dist/rules/types.js.map +1 -0
  145. package/dist/rules/variant-manager.d.ts +388 -0
  146. package/dist/rules/variant-manager.d.ts.map +1 -0
  147. package/dist/rules/variant-manager.js +777 -0
  148. package/dist/rules/variant-manager.js.map +1 -0
  149. package/dist/scanner/change-detector.d.ts +164 -0
  150. package/dist/scanner/change-detector.d.ts.map +1 -0
  151. package/dist/scanner/change-detector.js +263 -0
  152. package/dist/scanner/change-detector.js.map +1 -0
  153. package/dist/scanner/dependency-graph.d.ts +270 -0
  154. package/dist/scanner/dependency-graph.d.ts.map +1 -0
  155. package/dist/scanner/dependency-graph.js +436 -0
  156. package/dist/scanner/dependency-graph.js.map +1 -0
  157. package/dist/scanner/file-walker.d.ts +127 -0
  158. package/dist/scanner/file-walker.d.ts.map +1 -0
  159. package/dist/scanner/file-walker.js +526 -0
  160. package/dist/scanner/file-walker.js.map +1 -0
  161. package/dist/scanner/index.d.ts +12 -0
  162. package/dist/scanner/index.d.ts.map +1 -0
  163. package/dist/scanner/index.js +12 -0
  164. package/dist/scanner/index.js.map +1 -0
  165. package/dist/scanner/types.d.ts +218 -0
  166. package/dist/scanner/types.d.ts.map +1 -0
  167. package/dist/scanner/types.js +10 -0
  168. package/dist/scanner/types.js.map +1 -0
  169. package/dist/scanner/worker-pool.d.ts +317 -0
  170. package/dist/scanner/worker-pool.d.ts.map +1 -0
  171. package/dist/scanner/worker-pool.js +571 -0
  172. package/dist/scanner/worker-pool.js.map +1 -0
  173. package/dist/store/cache-manager.d.ts +179 -0
  174. package/dist/store/cache-manager.d.ts.map +1 -0
  175. package/dist/store/cache-manager.js +391 -0
  176. package/dist/store/cache-manager.js.map +1 -0
  177. package/dist/store/history-store.d.ts +314 -0
  178. package/dist/store/history-store.d.ts.map +1 -0
  179. package/dist/store/history-store.js +707 -0
  180. package/dist/store/history-store.js.map +1 -0
  181. package/dist/store/index.d.ts +20 -0
  182. package/dist/store/index.d.ts.map +1 -0
  183. package/dist/store/index.js +26 -0
  184. package/dist/store/index.js.map +1 -0
  185. package/dist/store/lock-file-manager.d.ts +202 -0
  186. package/dist/store/lock-file-manager.d.ts.map +1 -0
  187. package/dist/store/lock-file-manager.js +475 -0
  188. package/dist/store/lock-file-manager.js.map +1 -0
  189. package/dist/store/pattern-store.d.ts +289 -0
  190. package/dist/store/pattern-store.d.ts.map +1 -0
  191. package/dist/store/pattern-store.js +936 -0
  192. package/dist/store/pattern-store.js.map +1 -0
  193. package/dist/store/schema-validator.d.ts +159 -0
  194. package/dist/store/schema-validator.d.ts.map +1 -0
  195. package/dist/store/schema-validator.js +1096 -0
  196. package/dist/store/schema-validator.js.map +1 -0
  197. package/dist/store/types.d.ts +585 -0
  198. package/dist/store/types.d.ts.map +1 -0
  199. package/dist/store/types.js +82 -0
  200. package/dist/store/types.js.map +1 -0
  201. package/dist/types/analysis.d.ts +19 -0
  202. package/dist/types/analysis.d.ts.map +1 -0
  203. package/dist/types/analysis.js +5 -0
  204. package/dist/types/analysis.js.map +1 -0
  205. package/dist/types/common.d.ts +7 -0
  206. package/dist/types/common.d.ts.map +1 -0
  207. package/dist/types/common.js +5 -0
  208. package/dist/types/common.js.map +1 -0
  209. package/dist/types/index.d.ts +12 -0
  210. package/dist/types/index.d.ts.map +1 -0
  211. package/dist/types/index.js +10 -0
  212. package/dist/types/index.js.map +1 -0
  213. package/dist/types/patterns.d.ts +40 -0
  214. package/dist/types/patterns.d.ts.map +1 -0
  215. package/dist/types/patterns.js +7 -0
  216. package/dist/types/patterns.js.map +1 -0
  217. package/dist/types/violations.d.ts +7 -0
  218. package/dist/types/violations.d.ts.map +1 -0
  219. package/dist/types/violations.js +7 -0
  220. package/dist/types/violations.js.map +1 -0
  221. 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