@openrewrite/rewrite 8.66.1 → 8.66.3
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/java/tree.d.ts +10 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +21 -5
- package/dist/java/tree.js.map +1 -1
- package/dist/java/type-visitor.d.ts +1 -1
- package/dist/java/type-visitor.d.ts.map +1 -1
- package/dist/java/visitor.d.ts +2 -2
- package/dist/java/visitor.d.ts.map +1 -1
- package/dist/java/visitor.js +8 -2
- package/dist/java/visitor.js.map +1 -1
- package/dist/javascript/assertions.d.ts +6 -0
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +14 -6
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts +154 -7
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +623 -180
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format.d.ts +5 -3
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +85 -43
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/parser.d.ts +2 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +39 -30
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/templating/capture.d.ts +81 -14
- package/dist/javascript/templating/capture.d.ts.map +1 -1
- package/dist/javascript/templating/capture.js +98 -8
- package/dist/javascript/templating/capture.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts +125 -15
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +946 -118
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +58 -25
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +527 -94
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/index.d.ts +3 -3
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js +3 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +121 -16
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +528 -257
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +30 -5
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +183 -81
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts +56 -11
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +143 -16
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +31 -5
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +89 -15
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +359 -12
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +52 -35
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +107 -109
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +21 -11
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/json/rpc.js +2 -2
- package/dist/json/rpc.js.map +1 -1
- package/dist/recipe/order-imports.js.map +1 -1
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +10 -6
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/dist/visitor.d.ts +4 -4
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +8 -3
- package/dist/visitor.js.map +1 -1
- package/package.json +4 -2
- package/src/java/tree.ts +10 -3
- package/src/java/type-visitor.ts +1 -1
- package/src/java/visitor.ts +11 -5
- package/src/javascript/assertions.ts +9 -3
- package/src/javascript/comparator.ts +676 -185
- package/src/javascript/format.ts +72 -34
- package/src/javascript/index.ts +1 -0
- package/src/javascript/parser.ts +51 -31
- package/src/javascript/templating/capture.ts +107 -15
- package/src/javascript/templating/comparator.ts +1087 -134
- package/src/javascript/templating/engine.ts +601 -103
- package/src/javascript/templating/index.ts +9 -2
- package/src/javascript/templating/pattern.ts +655 -281
- package/src/javascript/templating/placeholder-replacement.ts +183 -80
- package/src/javascript/templating/rewrite.ts +152 -18
- package/src/javascript/templating/template.ts +110 -22
- package/src/javascript/templating/types.ts +386 -12
- package/src/javascript/templating/utils.ts +116 -102
- package/src/javascript/type-mapping.ts +20 -11
- package/src/json/rpc.ts +2 -2
- package/src/recipe/order-imports.ts +1 -1
- package/src/test/rewrite-test.ts +12 -7
- package/src/visitor.ts +14 -6
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import {Cursor} from '../..';
|
|
17
17
|
import {J} from '../../java';
|
|
18
|
+
import {Any, Capture, DebugLogEntry, DebugOptions, MatchAttemptResult, MatchExplanation, MatchOptions, PatternOptions, MatchResult as IMatchResult} from './types';
|
|
19
|
+
import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture';
|
|
20
|
+
import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator';
|
|
21
|
+
import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
22
|
+
import {TemplateEngine} from './engine';
|
|
23
|
+
import {TreePrinters} from '../../print';
|
|
18
24
|
import {JS} from '../index';
|
|
19
|
-
|
|
20
|
-
import {Capture, Any, PatternOptions} from './types';
|
|
21
|
-
import {CaptureImpl, CAPTURE_NAME_SYMBOL, CAPTURE_CAPTURING_SYMBOL} from './capture';
|
|
22
|
-
import {PatternMatchingComparator} from './comparator';
|
|
23
|
-
import {PlaceholderUtils, templateCache, CaptureMarker, CaptureStorageValue, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
25
|
+
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Builder for creating patterns programmatically.
|
|
@@ -48,7 +50,7 @@ import {PlaceholderUtils, templateCache, CaptureMarker, CaptureStorageValue, WRA
|
|
|
48
50
|
*/
|
|
49
51
|
export class PatternBuilder {
|
|
50
52
|
private parts: string[] = [];
|
|
51
|
-
private captures: (Capture | Any<any>)[] = [];
|
|
53
|
+
private captures: (Capture | Any<any> | RawCode)[] = [];
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Adds a static string part to the pattern.
|
|
@@ -73,15 +75,15 @@ export class PatternBuilder {
|
|
|
73
75
|
/**
|
|
74
76
|
* Adds a capture to the pattern.
|
|
75
77
|
*
|
|
76
|
-
* @param value The capture object (Capture or
|
|
78
|
+
* @param value The capture object (Capture, Any, or RawCode) or string name
|
|
77
79
|
* @returns This builder for chaining
|
|
78
80
|
*/
|
|
79
|
-
capture(value: Capture | Any<any> | string): this {
|
|
81
|
+
capture(value: Capture | Any<any> | RawCode | string): this {
|
|
80
82
|
// Ensure we have a part for after this capture
|
|
81
83
|
if (this.parts.length === 0) {
|
|
82
84
|
this.parts.push('');
|
|
83
85
|
}
|
|
84
|
-
// Convert string to Capture if needed
|
|
86
|
+
// Convert string to Capture if needed, or use value as-is for RawCode
|
|
85
87
|
const captureObj = typeof value === 'string' ? new CaptureImpl(value) : value;
|
|
86
88
|
this.captures.push(captureObj as any);
|
|
87
89
|
// Add an empty string for the next part
|
|
@@ -118,6 +120,10 @@ export class PatternBuilder {
|
|
|
118
120
|
*/
|
|
119
121
|
export class Pattern {
|
|
120
122
|
private _options: PatternOptions = {};
|
|
123
|
+
private _cachedAstPattern?: J;
|
|
124
|
+
private static nextPatternId = 1;
|
|
125
|
+
private readonly patternId: number;
|
|
126
|
+
private readonly unnamedCaptureMapping = new Map<string, string>();
|
|
121
127
|
|
|
122
128
|
/**
|
|
123
129
|
* Gets the configuration options for this pattern.
|
|
@@ -149,12 +155,25 @@ export class Pattern {
|
|
|
149
155
|
* Creates a new pattern from template parts and captures.
|
|
150
156
|
*
|
|
151
157
|
* @param templateParts The string parts of the template
|
|
152
|
-
* @param captures The captures between the string parts (can be Capture or
|
|
158
|
+
* @param captures The captures between the string parts (can be Capture, Any, or RawCode)
|
|
153
159
|
*/
|
|
154
160
|
constructor(
|
|
155
161
|
public readonly templateParts: TemplateStringsArray,
|
|
156
|
-
public readonly captures: (Capture | Any<any>)[]
|
|
162
|
+
public readonly captures: (Capture | Any<any> | RawCode)[]
|
|
157
163
|
) {
|
|
164
|
+
this.patternId = Pattern.nextPatternId++;
|
|
165
|
+
|
|
166
|
+
// Build mapping for unnamed captures (unnamed_N -> _X)
|
|
167
|
+
let unnamedIndex = 1;
|
|
168
|
+
for (const cap of captures) {
|
|
169
|
+
if (cap && typeof cap === 'object' && 'getName' in cap) {
|
|
170
|
+
const name = (cap as Capture<any> | Any<any>).getName();
|
|
171
|
+
if (name && name.startsWith('unnamed_')) {
|
|
172
|
+
this.unnamedCaptureMapping.set(name, `_${unnamedIndex}`);
|
|
173
|
+
unnamedIndex++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
/**
|
|
@@ -164,25 +183,115 @@ export class Pattern {
|
|
|
164
183
|
* @returns This pattern for method chaining
|
|
165
184
|
*
|
|
166
185
|
* @example
|
|
167
|
-
* pattern`
|
|
186
|
+
* pattern`forwardRef((${props}, ${ref}) => ${body})`
|
|
168
187
|
* .configure({
|
|
169
|
-
*
|
|
170
|
-
* dependencies: {
|
|
188
|
+
* context: ['import { forwardRef } from "react"'],
|
|
189
|
+
* dependencies: {'@types/react': '^18.0.0'}
|
|
171
190
|
* })
|
|
172
191
|
*/
|
|
173
192
|
configure(options: PatternOptions): Pattern {
|
|
174
|
-
this._options = {
|
|
193
|
+
this._options = {...this._options, ...options};
|
|
194
|
+
// Invalidate cache when configuration changes
|
|
195
|
+
this._cachedAstPattern = undefined;
|
|
175
196
|
return this;
|
|
176
197
|
}
|
|
177
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Gets the AST pattern for this pattern, using two-level caching:
|
|
201
|
+
* 1. Instance-level cache (fastest - this pattern instance)
|
|
202
|
+
* 2. Global LRU cache (fast - shared across pattern instances with same code)
|
|
203
|
+
* 3. Compute via TemplateProcessor (slow - parse and process)
|
|
204
|
+
*
|
|
205
|
+
* @returns The cached or newly computed pattern AST
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
async getAstPattern(): Promise<J> {
|
|
209
|
+
// Level 1: Instance cache (fastest path)
|
|
210
|
+
if (this._cachedAstPattern) {
|
|
211
|
+
return this._cachedAstPattern;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Generate cache key for global lookup
|
|
215
|
+
// Include raw code values in the key since they affect the generated AST
|
|
216
|
+
const contextStatements = this._options.context || this._options.imports || [];
|
|
217
|
+
const capturesKey = this.captures.map(c => {
|
|
218
|
+
if (c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) {
|
|
219
|
+
return `raw:${(c as RawCode).code}`;
|
|
220
|
+
}
|
|
221
|
+
return c.getName();
|
|
222
|
+
}).join(',');
|
|
223
|
+
const cacheKey = generateCacheKey(
|
|
224
|
+
this.templateParts,
|
|
225
|
+
capturesKey,
|
|
226
|
+
contextStatements,
|
|
227
|
+
this._options.dependencies || {}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Level 2: Global cache (fast path - shared with Template)
|
|
231
|
+
const cached = globalAstCache.get(cacheKey);
|
|
232
|
+
if (cached) {
|
|
233
|
+
this._cachedAstPattern = cached;
|
|
234
|
+
return cached;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Level 3: Compute via TemplateEngine (slow path)
|
|
238
|
+
const result = await TemplateEngine.getPatternTree(
|
|
239
|
+
this.templateParts,
|
|
240
|
+
this.captures,
|
|
241
|
+
contextStatements,
|
|
242
|
+
this._options.dependencies || {}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Cache in both levels
|
|
246
|
+
globalAstCache.set(cacheKey, result);
|
|
247
|
+
this._cachedAstPattern = result;
|
|
248
|
+
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
178
252
|
/**
|
|
179
253
|
* Creates a matcher for this pattern against a specific AST node.
|
|
180
254
|
*
|
|
181
255
|
* @param ast The AST node to match against
|
|
182
|
-
* @
|
|
256
|
+
* @param cursor Optional cursor at the node's position in a larger tree. Used for context-aware
|
|
257
|
+
* capture constraints to navigate to parent nodes. If omitted, a cursor will be
|
|
258
|
+
* created at the ast root, allowing constraints to navigate within the matched subtree.
|
|
259
|
+
* @param options Optional match options (e.g., debug flag)
|
|
260
|
+
* @returns A MatchResult if the pattern matches, undefined otherwise
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* // Normal match
|
|
265
|
+
* const match = await pattern.match(node);
|
|
266
|
+
*
|
|
267
|
+
* // Debug this specific call
|
|
268
|
+
* const match = await pattern.match(node, cursor, { debug: true });
|
|
269
|
+
* ```
|
|
183
270
|
*/
|
|
184
|
-
async match(ast: J): Promise<MatchResult | undefined> {
|
|
185
|
-
|
|
271
|
+
async match(ast: J, cursor?: Cursor, options?: MatchOptions): Promise<MatchResult | undefined> {
|
|
272
|
+
// Three-level precedence: call > pattern > global
|
|
273
|
+
const debugEnabled =
|
|
274
|
+
options?.debug !== undefined
|
|
275
|
+
? options.debug // 1. Explicit call-level (true OR false)
|
|
276
|
+
: (this._options.debug !== undefined
|
|
277
|
+
? this._options.debug // 2. Explicit pattern-level
|
|
278
|
+
: process.env.PATTERN_DEBUG === 'true'); // 3. Global
|
|
279
|
+
|
|
280
|
+
if (debugEnabled) {
|
|
281
|
+
// Use matchWithExplanation and log the result
|
|
282
|
+
const result = await this.matchWithExplanation(ast, cursor);
|
|
283
|
+
await this.logMatchResult(ast, cursor, result);
|
|
284
|
+
|
|
285
|
+
if (result.matched) {
|
|
286
|
+
// result.result is the MatchResult class instance
|
|
287
|
+
return result.result as MatchResult | undefined;
|
|
288
|
+
} else {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fast path - no debug
|
|
294
|
+
const matcher = new Matcher(this, ast, cursor);
|
|
186
295
|
const success = await matcher.matches();
|
|
187
296
|
if (!success) {
|
|
188
297
|
return undefined;
|
|
@@ -191,6 +300,265 @@ export class Pattern {
|
|
|
191
300
|
const storage = (matcher as any).storage;
|
|
192
301
|
return new MatchResult(new Map(storage));
|
|
193
302
|
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Formats and logs the match result to stderr.
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
private async logMatchResult(ast: J, cursor: Cursor | undefined, result: MatchAttemptResult): Promise<void> {
|
|
309
|
+
const patternSource = this.getPatternSource();
|
|
310
|
+
const patternId = `Pattern #${this.patternId}`;
|
|
311
|
+
const nodeKind = (ast as any).kind || 'unknown';
|
|
312
|
+
// Format kind: extract short name (e.g., "org.openrewrite.java.tree.J$Binary" -> "J$Binary")
|
|
313
|
+
const shortKind = typeof nodeKind === 'string'
|
|
314
|
+
? nodeKind.split('.').pop() || nodeKind
|
|
315
|
+
: nodeKind;
|
|
316
|
+
|
|
317
|
+
// First, log the pattern source
|
|
318
|
+
console.error(`[${patternId}] ${patternSource}`);
|
|
319
|
+
|
|
320
|
+
// Build the complete match result message
|
|
321
|
+
const lines: string[] = [];
|
|
322
|
+
|
|
323
|
+
// Print the target tree being matched
|
|
324
|
+
let treeStr: string;
|
|
325
|
+
try {
|
|
326
|
+
const printer = TreePrinters.printer(JS.Kind.CompilationUnit);
|
|
327
|
+
treeStr = await printer.print(ast);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
treeStr = '(tree printing unavailable)';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (result.matched) {
|
|
333
|
+
// Success case - result first, then tree, then captures
|
|
334
|
+
lines.push(`[${patternId}] ✅ SUCCESS matching against ${shortKind}:`);
|
|
335
|
+
treeStr.split('\n').forEach(line => lines.push(`[${patternId}] ${line}`));
|
|
336
|
+
|
|
337
|
+
// Log captured values
|
|
338
|
+
if (result.result) {
|
|
339
|
+
const storage = (result.result as any).storage as Map<string, CaptureStorageValue>;
|
|
340
|
+
if (storage && storage.size > 0) {
|
|
341
|
+
for (const [name, value] of storage) {
|
|
342
|
+
const extractedValue = (result.result as any).extractElements(value);
|
|
343
|
+
const valueStr = this.formatCapturedValue(extractedValue);
|
|
344
|
+
const displayName = this.unnamedCaptureMapping.get(name) || name;
|
|
345
|
+
lines.push(`[${patternId}] Captured '${displayName}': ${valueStr}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// Failure case - result first, then tree, then explanation
|
|
351
|
+
lines.push(`[${patternId}] ❌ FAILED matching against ${shortKind}:`);
|
|
352
|
+
treeStr.split('\n').forEach(line => lines.push(`[${patternId}] ${line}`));
|
|
353
|
+
|
|
354
|
+
const explanation = result.explanation;
|
|
355
|
+
if (explanation) {
|
|
356
|
+
// Always show path, even if empty, to make it clear where the mismatch occurred
|
|
357
|
+
const compactedPath = this.compactPath(explanation.path);
|
|
358
|
+
const pathStr = compactedPath.length > 0 ? compactedPath.join(' → ') : '';
|
|
359
|
+
lines.push(`[${patternId}] At path: [${pathStr}]`);
|
|
360
|
+
lines.push(`[${patternId}] Reason: ${explanation.reason}`);
|
|
361
|
+
lines.push(`[${patternId}] Expected: ${explanation.expected}`);
|
|
362
|
+
lines.push(`[${patternId}] Actual: ${explanation.actual}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Single console.error call with all lines joined
|
|
367
|
+
console.error(lines.join('\n'));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Compacts array index navigations into the previous path element.
|
|
372
|
+
* For example: ['J$VariableDeclarations#variables', '0'] → ['J$VariableDeclarations#variables[0]']
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
private compactPath(path: string[]): string[] {
|
|
376
|
+
const compacted: string[] = [];
|
|
377
|
+
let i = 0;
|
|
378
|
+
|
|
379
|
+
while (i < path.length) {
|
|
380
|
+
const current = path[i];
|
|
381
|
+
|
|
382
|
+
// Check if current element is itself a numeric index
|
|
383
|
+
if (/^\d+$/.test(current)) {
|
|
384
|
+
// This is a bare numeric index - shouldn't normally happen
|
|
385
|
+
// If we have a previous element, append to it
|
|
386
|
+
if (compacted.length > 0) {
|
|
387
|
+
compacted[compacted.length - 1] += `[${current}]`;
|
|
388
|
+
} else {
|
|
389
|
+
// No previous element to attach to - this is an error in path construction
|
|
390
|
+
// Skip it to avoid bare [0] in output
|
|
391
|
+
console.warn(`Warning: Path starts with numeric index '${current}' - skipping`);
|
|
392
|
+
}
|
|
393
|
+
i++;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Look ahead to collect consecutive numeric indices
|
|
398
|
+
let j = i + 1;
|
|
399
|
+
const indices: string[] = [];
|
|
400
|
+
while (j < path.length && /^\d+$/.test(path[j])) {
|
|
401
|
+
indices.push(path[j]);
|
|
402
|
+
j++;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// If we found numeric indices, append them to current element
|
|
406
|
+
if (indices.length > 0) {
|
|
407
|
+
compacted.push(current + indices.map(idx => `[${idx}]`).join(''));
|
|
408
|
+
i = j; // Skip the indices we just processed
|
|
409
|
+
} else {
|
|
410
|
+
compacted.push(current);
|
|
411
|
+
i++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return compacted;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Gets the source code representation of this pattern for logging.
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
private getPatternSource(): string {
|
|
423
|
+
// Reconstruct pattern source from template parts
|
|
424
|
+
let source = '';
|
|
425
|
+
for (let i = 0; i < this.templateParts.length; i++) {
|
|
426
|
+
source += this.templateParts[i];
|
|
427
|
+
if (i < this.captures.length) {
|
|
428
|
+
const cap = this.captures[i];
|
|
429
|
+
// Skip raw code
|
|
430
|
+
if (cap instanceof RawCode || (cap && typeof cap === 'object' && (cap as any)[RAW_CODE_SYMBOL])) {
|
|
431
|
+
source += '${raw(...)}';
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
// Show capture name or placeholder
|
|
435
|
+
const name = (cap as any)[CAPTURE_NAME_SYMBOL];
|
|
436
|
+
if (cap && typeof cap === 'object' && name) {
|
|
437
|
+
// Use mapped name for unnamed captures, or original name
|
|
438
|
+
const displayName = this.unnamedCaptureMapping.get(name) || name;
|
|
439
|
+
source += `\${${displayName}}`;
|
|
440
|
+
} else {
|
|
441
|
+
source += '${...}';
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return source;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Formats a captured value for logging.
|
|
451
|
+
* @private
|
|
452
|
+
*/
|
|
453
|
+
private formatCapturedValue(value: any): string {
|
|
454
|
+
if (value === null) return 'null';
|
|
455
|
+
if (value === undefined) return 'undefined';
|
|
456
|
+
|
|
457
|
+
// Check if it's an array (variadic capture)
|
|
458
|
+
if (Array.isArray(value)) {
|
|
459
|
+
if (value.length === 0) return '[]';
|
|
460
|
+
const items = value.slice(0, 3).map(v => this.formatSingleValue(v));
|
|
461
|
+
const suffix = value.length > 3 ? `, ... (${value.length} total)` : '';
|
|
462
|
+
return `[${items.join(', ')}${suffix}]`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return this.formatSingleValue(value);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Formats a single AST node for logging.
|
|
470
|
+
* @private
|
|
471
|
+
*/
|
|
472
|
+
private formatSingleValue(value: any): string {
|
|
473
|
+
if (!value || typeof value !== 'object') {
|
|
474
|
+
return String(value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const kind = (value as any).kind;
|
|
478
|
+
if (!kind) return String(value);
|
|
479
|
+
|
|
480
|
+
// Extract simple kind name (last segment)
|
|
481
|
+
const kindStr = kind.split('.').pop();
|
|
482
|
+
|
|
483
|
+
// For literals, show the value
|
|
484
|
+
if (kindStr === 'Literal' && value.value !== undefined) {
|
|
485
|
+
const litValue = typeof value.value === 'string'
|
|
486
|
+
? `"${value.value}"`
|
|
487
|
+
: String(value.value);
|
|
488
|
+
return `${kindStr}(${litValue})`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// For identifiers, show the name
|
|
492
|
+
if (kindStr === 'Identifier' && value.simpleName) {
|
|
493
|
+
return `${kindStr}(${value.simpleName})`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Default: just the kind
|
|
497
|
+
return kindStr;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Matches a pattern against an AST node with detailed debug information.
|
|
502
|
+
* Part of Layer 2 (Public API).
|
|
503
|
+
*
|
|
504
|
+
* This method always enables debug logging and returns detailed information about
|
|
505
|
+
* the match attempt, including:
|
|
506
|
+
* - Whether the pattern matched
|
|
507
|
+
* - Captured nodes (if matched)
|
|
508
|
+
* - Explanation of failure (if not matched)
|
|
509
|
+
* - Debug log entries showing the matching process
|
|
510
|
+
*
|
|
511
|
+
* @param ast The AST node to match against
|
|
512
|
+
* @param cursor Optional cursor at the node's position in a larger tree
|
|
513
|
+
* @param debugOptions Optional debug options (defaults to all logging enabled)
|
|
514
|
+
* @returns Detailed result with debug information
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* const x = capture('x');
|
|
518
|
+
* const pat = pattern`console.log(${x})`;
|
|
519
|
+
* const attempt = await pat.matchWithExplanation(node);
|
|
520
|
+
* if (attempt.matched) {
|
|
521
|
+
* console.log('Matched!');
|
|
522
|
+
* console.log('Captured x:', attempt.result.get('x'));
|
|
523
|
+
* } else {
|
|
524
|
+
* console.log('Failed:', attempt.explanation);
|
|
525
|
+
* console.log('Debug log:', attempt.debugLog);
|
|
526
|
+
* }
|
|
527
|
+
*/
|
|
528
|
+
async matchWithExplanation(
|
|
529
|
+
ast: J,
|
|
530
|
+
cursor?: Cursor,
|
|
531
|
+
debugOptions?: DebugOptions
|
|
532
|
+
): Promise<MatchAttemptResult> {
|
|
533
|
+
// Default to full debug logging if not specified
|
|
534
|
+
const options: DebugOptions = {
|
|
535
|
+
enabled: true,
|
|
536
|
+
logComparison: true,
|
|
537
|
+
logConstraints: true,
|
|
538
|
+
...debugOptions
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const matcher = new Matcher(this, ast, cursor, options);
|
|
542
|
+
const success = await matcher.matches();
|
|
543
|
+
|
|
544
|
+
if (success) {
|
|
545
|
+
// Match succeeded - return MatchResult with debug info
|
|
546
|
+
const storage = (matcher as any).storage;
|
|
547
|
+
const matchResult = new MatchResult(new Map(storage));
|
|
548
|
+
return {
|
|
549
|
+
matched: true,
|
|
550
|
+
result: matchResult,
|
|
551
|
+
debugLog: matcher.getDebugLog()
|
|
552
|
+
};
|
|
553
|
+
} else {
|
|
554
|
+
// Match failed - return explanation
|
|
555
|
+
return {
|
|
556
|
+
matched: false,
|
|
557
|
+
explanation: matcher.getExplanation(),
|
|
558
|
+
debugLog: matcher.getDebugLog()
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
194
562
|
}
|
|
195
563
|
|
|
196
564
|
/**
|
|
@@ -218,18 +586,16 @@ export class Pattern {
|
|
|
218
586
|
* const capturedArgs = match.get(args); // Returns J[] for variadic captures
|
|
219
587
|
* }
|
|
220
588
|
*/
|
|
221
|
-
export class MatchResult implements
|
|
589
|
+
export class MatchResult implements IMatchResult {
|
|
222
590
|
constructor(
|
|
223
591
|
private readonly storage: Map<string, CaptureStorageValue> = new Map()
|
|
224
592
|
) {
|
|
225
593
|
}
|
|
226
594
|
|
|
227
|
-
// Overload: get with
|
|
228
|
-
get<T>(capture: Capture<T[]>): T[] | undefined;
|
|
229
|
-
// Overload: get with regular Capture returns single value
|
|
595
|
+
// Overload: get with Capture returns value
|
|
230
596
|
get<T>(capture: Capture<T>): T | undefined;
|
|
231
|
-
// Overload: get with string returns
|
|
232
|
-
get(capture: string):
|
|
597
|
+
// Overload: get with string returns value
|
|
598
|
+
get(capture: string): any;
|
|
233
599
|
// Implementation
|
|
234
600
|
get(capture: Capture<any> | string): J | J[] | undefined {
|
|
235
601
|
// Use symbol to get internal name without triggering Proxy
|
|
@@ -294,18 +660,33 @@ class Matcher {
|
|
|
294
660
|
private readonly storage = new Map<string, CaptureStorageValue>();
|
|
295
661
|
private patternAst?: J;
|
|
296
662
|
|
|
663
|
+
// Debug tracking (Layer 1: Core Instrumentation)
|
|
664
|
+
private readonly debugOptions: DebugOptions;
|
|
665
|
+
private readonly debugLog: DebugLogEntry[] = [];
|
|
666
|
+
private explanation?: MatchExplanation;
|
|
667
|
+
private readonly currentPath: string[] = [];
|
|
668
|
+
|
|
297
669
|
/**
|
|
298
670
|
* Creates a new matcher for a pattern against an AST node.
|
|
299
671
|
*
|
|
300
672
|
* @param pattern The pattern to match
|
|
301
673
|
* @param ast The AST node to match against
|
|
674
|
+
* @param cursor Optional cursor at the AST node's position
|
|
675
|
+
* @param debugOptions Optional debug options for instrumentation
|
|
302
676
|
*/
|
|
303
677
|
constructor(
|
|
304
678
|
private readonly pattern: Pattern,
|
|
305
|
-
private readonly ast: J
|
|
679
|
+
private readonly ast: J,
|
|
680
|
+
cursor?: Cursor,
|
|
681
|
+
debugOptions?: DebugOptions
|
|
306
682
|
) {
|
|
683
|
+
// If no cursor provided, create one at the ast root so constraints can navigate up
|
|
684
|
+
this.cursor = cursor ?? new Cursor(ast, undefined);
|
|
685
|
+
this.debugOptions = debugOptions ?? {};
|
|
307
686
|
}
|
|
308
687
|
|
|
688
|
+
private readonly cursor: Cursor;
|
|
689
|
+
|
|
309
690
|
/**
|
|
310
691
|
* Checks if the pattern matches the AST node.
|
|
311
692
|
*
|
|
@@ -313,15 +694,7 @@ class Matcher {
|
|
|
313
694
|
*/
|
|
314
695
|
async matches(): Promise<boolean> {
|
|
315
696
|
if (!this.patternAst) {
|
|
316
|
-
|
|
317
|
-
const contextStatements = this.pattern.options.context || this.pattern.options.imports || [];
|
|
318
|
-
const templateProcessor = new TemplateProcessor(
|
|
319
|
-
this.pattern.templateParts,
|
|
320
|
-
this.pattern.captures,
|
|
321
|
-
contextStatements,
|
|
322
|
-
this.pattern.options.dependencies || {}
|
|
323
|
-
);
|
|
324
|
-
this.patternAst = await templateProcessor.toAstPattern();
|
|
697
|
+
this.patternAst = await this.pattern.getAstPattern();
|
|
325
698
|
}
|
|
326
699
|
|
|
327
700
|
return this.matchNode(this.patternAst, this.ast);
|
|
@@ -365,6 +738,83 @@ class Matcher {
|
|
|
365
738
|
return value as J;
|
|
366
739
|
}
|
|
367
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Logs a debug message if debugging is enabled.
|
|
743
|
+
* Part of Layer 1 (Core Instrumentation).
|
|
744
|
+
*
|
|
745
|
+
* @param level The severity level
|
|
746
|
+
* @param scope The scope/category
|
|
747
|
+
* @param message The message to log
|
|
748
|
+
* @param data Optional data to include
|
|
749
|
+
*/
|
|
750
|
+
private log(
|
|
751
|
+
level: DebugLogEntry['level'],
|
|
752
|
+
scope: DebugLogEntry['scope'],
|
|
753
|
+
message: string,
|
|
754
|
+
data?: any
|
|
755
|
+
): void {
|
|
756
|
+
if (!this.debugOptions.enabled) return;
|
|
757
|
+
|
|
758
|
+
// Filter by scope if specific logging is requested
|
|
759
|
+
if (scope === 'comparison' && !this.debugOptions.logComparison) return;
|
|
760
|
+
if (scope === 'constraint' && !this.debugOptions.logConstraints) return;
|
|
761
|
+
|
|
762
|
+
this.debugLog.push({
|
|
763
|
+
level,
|
|
764
|
+
scope,
|
|
765
|
+
path: [...this.currentPath],
|
|
766
|
+
message,
|
|
767
|
+
data
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Sets the explanation for why the pattern match failed.
|
|
773
|
+
* Only sets the first failure (most relevant).
|
|
774
|
+
* Part of Layer 1 (Core Instrumentation).
|
|
775
|
+
*
|
|
776
|
+
* @param reason The reason for failure
|
|
777
|
+
* @param expected Human-readable description of what was expected
|
|
778
|
+
* @param actual Human-readable description of what was found
|
|
779
|
+
* @param details Optional additional context
|
|
780
|
+
*/
|
|
781
|
+
private setExplanation(
|
|
782
|
+
reason: MatchExplanation['reason'],
|
|
783
|
+
expected: string,
|
|
784
|
+
actual: string,
|
|
785
|
+
details?: string
|
|
786
|
+
): void {
|
|
787
|
+
// Only set the first failure (most relevant)
|
|
788
|
+
if (this.explanation) return;
|
|
789
|
+
|
|
790
|
+
this.explanation = {
|
|
791
|
+
reason,
|
|
792
|
+
path: [...this.currentPath],
|
|
793
|
+
expected,
|
|
794
|
+
actual,
|
|
795
|
+
details
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Pushes a path component onto the current path.
|
|
801
|
+
* Used to track where in the AST tree we are during matching.
|
|
802
|
+
* Part of Layer 1 (Core Instrumentation).
|
|
803
|
+
*
|
|
804
|
+
* @param name The path component to push
|
|
805
|
+
*/
|
|
806
|
+
private pushPath(name: string): void {
|
|
807
|
+
this.currentPath.push(name);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Pops the last path component from the current path.
|
|
812
|
+
* Part of Layer 1 (Core Instrumentation).
|
|
813
|
+
*/
|
|
814
|
+
private popPath(): void {
|
|
815
|
+
this.currentPath.pop();
|
|
816
|
+
}
|
|
817
|
+
|
|
368
818
|
/**
|
|
369
819
|
* Matches a pattern node against a target node.
|
|
370
820
|
*
|
|
@@ -373,70 +823,117 @@ class Matcher {
|
|
|
373
823
|
* @returns true if the pattern matches the target, false otherwise
|
|
374
824
|
*/
|
|
375
825
|
private async matchNode(pattern: J, target: J): Promise<boolean> {
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
826
|
+
// Always delegate to the comparator visitor, which handles:
|
|
827
|
+
// - Capture detection and constraint evaluation
|
|
828
|
+
// - Kind checking
|
|
829
|
+
// - Deep structural comparison
|
|
830
|
+
// This centralizes all matching logic in one place
|
|
831
|
+
const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
|
|
380
832
|
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
833
|
+
// Factory pattern: instantiate debug or production comparator
|
|
834
|
+
// Zero cost in production - DebugPatternMatchingComparator is never instantiated
|
|
835
|
+
const matcherCallbacks: MatcherCallbacks = {
|
|
836
|
+
handleCapture: (capture: CaptureMarker, t: J, w?: J.RightPadded<J>) => this.handleCapture(capture, t, w),
|
|
837
|
+
handleVariadicCapture: (capture: CaptureMarker, ts: J[], ws?: J.RightPadded<J>[]) => this.handleVariadicCapture(capture, ts, ws),
|
|
838
|
+
saveState: () => this.saveState(),
|
|
839
|
+
restoreState: (state) => this.restoreState(state),
|
|
840
|
+
// Debug callbacks (Layer 1) - grouped together, always present or absent
|
|
841
|
+
debug: this.debugOptions.enabled ? {
|
|
842
|
+
log: (level: DebugLogEntry['level'], scope: DebugLogEntry['scope'], message: string, data?: any) => this.log(level, scope, message, data),
|
|
843
|
+
setExplanation: (reason: MatchExplanation['reason'], expected: string, actual: string, details?: string) => this.setExplanation(reason, expected, actual, details),
|
|
844
|
+
getExplanation: () => this.explanation,
|
|
845
|
+
restoreExplanation: (explanation: MatchExplanation) => { this.explanation = explanation; },
|
|
846
|
+
clearExplanation: () => { this.explanation = undefined; },
|
|
847
|
+
pushPath: (name: string) => this.pushPath(name),
|
|
848
|
+
popPath: () => this.popPath()
|
|
849
|
+
} : undefined
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const comparator = this.debugOptions.enabled
|
|
853
|
+
? new DebugPatternMatchingComparator(matcherCallbacks, lenientTypeMatching)
|
|
854
|
+
: new PatternMatchingComparator(matcherCallbacks, lenientTypeMatching);
|
|
855
|
+
// Pass cursors to allow constraints to navigate to root
|
|
856
|
+
// Pattern cursor is undefined (pattern is the root), target cursor is provided by user
|
|
857
|
+
const result = await comparator.compare(pattern, target, undefined, this.cursor);
|
|
858
|
+
|
|
859
|
+
// If match failed and no explanation was set, provide a generic one
|
|
860
|
+
if (!result && this.debugOptions.enabled && !this.explanation) {
|
|
861
|
+
const patternKind = (pattern as any).kind?.split('.').pop() || 'unknown';
|
|
862
|
+
const targetKind = (target as any).kind?.split('.').pop() || 'unknown';
|
|
863
|
+
this.setExplanation(
|
|
864
|
+
'structural-mismatch',
|
|
865
|
+
`Pattern node of type ${patternKind}`,
|
|
866
|
+
`Target node of type ${targetKind}`,
|
|
867
|
+
'Nodes did not match structurally'
|
|
868
|
+
);
|
|
384
869
|
}
|
|
385
870
|
|
|
386
|
-
|
|
387
|
-
// Default to true for backward compatibility with existing patterns
|
|
388
|
-
const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
|
|
389
|
-
const comparator = new PatternMatchingComparator({
|
|
390
|
-
handleCapture: (p, t) => this.handleCapture(p, t),
|
|
391
|
-
handleVariadicCapture: (p, ts, ws) => this.handleVariadicCapture(p, ts, ws),
|
|
392
|
-
saveState: () => this.saveState(),
|
|
393
|
-
restoreState: (state) => this.restoreState(state)
|
|
394
|
-
}, lenientTypeMatching);
|
|
395
|
-
return await comparator.compare(pattern, target);
|
|
871
|
+
return result;
|
|
396
872
|
}
|
|
397
873
|
|
|
398
874
|
/**
|
|
399
|
-
* Saves the current state
|
|
875
|
+
* Saves the current state for backtracking.
|
|
876
|
+
* Includes both capture storage AND debug state (explanation, log, path).
|
|
400
877
|
*
|
|
401
878
|
* @returns A snapshot of the current state
|
|
402
879
|
*/
|
|
403
|
-
private saveState():
|
|
404
|
-
return
|
|
880
|
+
private saveState(): MatcherState {
|
|
881
|
+
return {
|
|
882
|
+
storage: new Map(this.storage),
|
|
883
|
+
debugState: this.debugOptions.enabled ? {
|
|
884
|
+
explanation: this.explanation,
|
|
885
|
+
logLength: this.debugLog.length,
|
|
886
|
+
path: [...this.currentPath]
|
|
887
|
+
} : undefined
|
|
888
|
+
};
|
|
405
889
|
}
|
|
406
890
|
|
|
407
891
|
/**
|
|
408
892
|
* Restores a previously saved state for backtracking.
|
|
893
|
+
* Restores both capture storage AND debug state.
|
|
409
894
|
*
|
|
410
895
|
* @param state The state to restore
|
|
411
896
|
*/
|
|
412
|
-
private restoreState(state:
|
|
897
|
+
private restoreState(state: MatcherState): void {
|
|
898
|
+
// Restore capture storage
|
|
413
899
|
this.storage.clear();
|
|
414
|
-
state.forEach((value, key) => this.storage.set(key, value));
|
|
900
|
+
state.storage.forEach((value, key) => this.storage.set(key, value));
|
|
901
|
+
|
|
902
|
+
// Restore debug state if it was saved
|
|
903
|
+
if (state.debugState) {
|
|
904
|
+
// Restore explanation to the saved state
|
|
905
|
+
// This clears any explanations set during failed exploratory attempts (like pivot detection)
|
|
906
|
+
this.explanation = state.debugState.explanation;
|
|
907
|
+
// Truncate debug log to saved length (remove entries added during failed attempt)
|
|
908
|
+
this.debugLog.length = state.debugState.logLength;
|
|
909
|
+
// Restore path
|
|
910
|
+
this.currentPath.length = 0;
|
|
911
|
+
this.currentPath.push(...state.debugState.path);
|
|
912
|
+
}
|
|
415
913
|
}
|
|
416
914
|
|
|
417
915
|
/**
|
|
418
916
|
* Handles a capture placeholder.
|
|
419
917
|
*
|
|
420
|
-
* @param
|
|
918
|
+
* @param capture The pattern node capture
|
|
421
919
|
* @param target The target node
|
|
422
920
|
* @param wrapper Optional wrapper containing the target (for preserving markers)
|
|
423
921
|
* @returns true if the capture is successful, false otherwise
|
|
424
922
|
*/
|
|
425
|
-
private handleCapture(
|
|
426
|
-
const captureName =
|
|
923
|
+
private handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>): boolean {
|
|
924
|
+
const captureName = capture.captureName;
|
|
427
925
|
|
|
428
926
|
if (!captureName) {
|
|
429
927
|
return false;
|
|
430
928
|
}
|
|
431
929
|
|
|
432
|
-
// Find the original capture object to get
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
930
|
+
// Find the original capture object to get capturing flag
|
|
931
|
+
// Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned
|
|
932
|
+
// Filter out RawCode since it doesn't have getName()
|
|
933
|
+
const captureObj = this.pattern.captures.find(c =>
|
|
934
|
+
!(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) &&
|
|
935
|
+
c.getName() === captureName
|
|
936
|
+
);
|
|
440
937
|
|
|
441
938
|
// Only store the binding if this is a capturing placeholder
|
|
442
939
|
const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
|
|
@@ -451,26 +948,25 @@ class Matcher {
|
|
|
451
948
|
/**
|
|
452
949
|
* Handles a variadic capture placeholder.
|
|
453
950
|
*
|
|
454
|
-
* @param
|
|
951
|
+
* @param capture The pattern node capture (the variadic capture)
|
|
455
952
|
* @param targets The target nodes that were matched
|
|
456
953
|
* @param wrappers Optional wrappers to preserve markers
|
|
457
954
|
* @returns true if the capture is successful, false otherwise
|
|
458
955
|
*/
|
|
459
|
-
private handleVariadicCapture(
|
|
460
|
-
const captureName =
|
|
956
|
+
private handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
|
|
957
|
+
const captureName = capture.captureName;
|
|
461
958
|
|
|
462
959
|
if (!captureName) {
|
|
463
960
|
return false;
|
|
464
961
|
}
|
|
465
962
|
|
|
466
|
-
// Find the original capture object to get
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
963
|
+
// Find the original capture object to get capturing flag
|
|
964
|
+
// Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned
|
|
965
|
+
// Filter out RawCode since it doesn't have getName()
|
|
966
|
+
const captureObj = this.pattern.captures.find(c =>
|
|
967
|
+
!(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) &&
|
|
968
|
+
c.getName() === captureName
|
|
969
|
+
);
|
|
474
970
|
|
|
475
971
|
// Only store the binding if this is a capturing placeholder
|
|
476
972
|
const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
|
|
@@ -485,214 +981,25 @@ class Matcher {
|
|
|
485
981
|
|
|
486
982
|
return true;
|
|
487
983
|
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Processor for template strings.
|
|
492
|
-
* Converts a template string with captures into an AST pattern.
|
|
493
|
-
*/
|
|
494
|
-
class TemplateProcessor {
|
|
495
|
-
/**
|
|
496
|
-
* Creates a new template processor.
|
|
497
|
-
*
|
|
498
|
-
* @param templateParts The string parts of the template
|
|
499
|
-
* @param captures The captures between the string parts (can be Capture or Any)
|
|
500
|
-
* @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
|
|
501
|
-
* @param dependencies NPM dependencies for type attribution
|
|
502
|
-
*/
|
|
503
|
-
constructor(
|
|
504
|
-
private readonly templateParts: TemplateStringsArray,
|
|
505
|
-
private readonly captures: (Capture | Any<any>)[],
|
|
506
|
-
private readonly contextStatements: string[] = [],
|
|
507
|
-
private readonly dependencies: Record<string, string> = {}
|
|
508
|
-
) {
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Converts the template to an AST pattern.
|
|
513
|
-
*
|
|
514
|
-
* @returns A Promise resolving to the AST pattern
|
|
515
|
-
*/
|
|
516
|
-
async toAstPattern(): Promise<J> {
|
|
517
|
-
// Combine template parts and placeholders
|
|
518
|
-
const templateString = this.buildTemplateString();
|
|
519
|
-
|
|
520
|
-
// Use cache to get or parse the compilation unit
|
|
521
|
-
const cu = await templateCache.getOrParse(
|
|
522
|
-
templateString,
|
|
523
|
-
this.captures,
|
|
524
|
-
this.contextStatements,
|
|
525
|
-
this.dependencies
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// Extract the relevant part of the AST
|
|
529
|
-
return this.extractPatternFromAst(cu);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Builds a template string with placeholders for captures.
|
|
534
|
-
* If the template looks like a block pattern, wraps it in a function.
|
|
535
|
-
*
|
|
536
|
-
* @returns The template string
|
|
537
|
-
*/
|
|
538
|
-
private buildTemplateString(): string {
|
|
539
|
-
let result = '';
|
|
540
|
-
for (let i = 0; i < this.templateParts.length; i++) {
|
|
541
|
-
result += this.templateParts[i];
|
|
542
|
-
if (i < this.captures.length) {
|
|
543
|
-
const capture = this.captures[i];
|
|
544
|
-
// Use symbol to access capture name without triggering Proxy
|
|
545
|
-
const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
546
|
-
result += PlaceholderUtils.createCapture(captureName, undefined);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Check if this looks like a block pattern (starts with { and contains statement keywords)
|
|
551
|
-
const trimmed = result.trim();
|
|
552
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
553
|
-
// Check for statement keywords that indicate this is a block, not an object literal
|
|
554
|
-
const hasStatementKeywords = /\b(return|if|for|while|do|switch|try|throw|break|continue|const|let|var|function|class)\b/.test(result);
|
|
555
|
-
if (hasStatementKeywords) {
|
|
556
|
-
// Wrap in a function to ensure it parses as a block
|
|
557
|
-
return `function __PATTERN__() ${result}`;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return result;
|
|
562
|
-
}
|
|
563
984
|
|
|
564
985
|
/**
|
|
565
|
-
*
|
|
986
|
+
* Gets the debug log entries collected during matching.
|
|
987
|
+
* Part of Layer 2 (Public API).
|
|
566
988
|
*
|
|
567
|
-
* @
|
|
568
|
-
* @returns The extracted pattern
|
|
569
|
-
*/
|
|
570
|
-
private extractPatternFromAst(cu: JS.CompilationUnit): J {
|
|
571
|
-
// Skip context statements to get to the actual pattern code
|
|
572
|
-
const patternStatementIndex = this.contextStatements.length;
|
|
573
|
-
|
|
574
|
-
// Check if we have any statements at the pattern index
|
|
575
|
-
if (!cu.statements || patternStatementIndex >= cu.statements.length) {
|
|
576
|
-
// If there's no statement at the index, but we have exactly one statement
|
|
577
|
-
// and it's a block, it might be the pattern itself (e.g., pattern`{ ... }`)
|
|
578
|
-
if (cu.statements && cu.statements.length === 1 && cu.statements[0].element.kind === J.Kind.Block) {
|
|
579
|
-
return this.attachCaptureMarkers(cu.statements[0].element);
|
|
580
|
-
}
|
|
581
|
-
throw new Error(`No statement found at index ${patternStatementIndex} in compilation unit with ${cu.statements?.length || 0} statements`);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Extract the relevant part of the AST based on the template content
|
|
585
|
-
const firstStatement = cu.statements[patternStatementIndex].element;
|
|
586
|
-
|
|
587
|
-
let extracted: J;
|
|
588
|
-
|
|
589
|
-
// Check if this is our wrapper function for block patterns
|
|
590
|
-
if (firstStatement.kind === J.Kind.MethodDeclaration) {
|
|
591
|
-
const method = firstStatement as J.MethodDeclaration;
|
|
592
|
-
if (method.name?.simpleName === '__PATTERN__' && method.body) {
|
|
593
|
-
// Extract the block from the wrapper function
|
|
594
|
-
extracted = method.body;
|
|
595
|
-
} else {
|
|
596
|
-
extracted = firstStatement;
|
|
597
|
-
}
|
|
598
|
-
} else if (firstStatement.kind === JS.Kind.ExpressionStatement) {
|
|
599
|
-
// If the first statement is an expression statement, extract the expression
|
|
600
|
-
extracted = (firstStatement as JS.ExpressionStatement).expression;
|
|
601
|
-
} else {
|
|
602
|
-
// Otherwise, return the statement itself
|
|
603
|
-
extracted = firstStatement;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// Attach CaptureMarkers to capture identifiers
|
|
607
|
-
return this.attachCaptureMarkers(extracted);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Attaches CaptureMarkers to capture identifiers in the AST.
|
|
612
|
-
* This allows efficient capture detection without string parsing.
|
|
613
|
-
*
|
|
614
|
-
* @param ast The AST to process
|
|
615
|
-
* @returns The AST with CaptureMarkers attached
|
|
989
|
+
* @returns The debug log entries, or undefined if debug wasn't enabled
|
|
616
990
|
*/
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
return produce(ast, draft => {
|
|
620
|
-
this.visitAndAttachMarkers(draft, visited);
|
|
621
|
-
});
|
|
991
|
+
getDebugLog(): DebugLogEntry[] | undefined {
|
|
992
|
+
return this.debugOptions.enabled ? [...this.debugLog] : undefined;
|
|
622
993
|
}
|
|
623
994
|
|
|
624
995
|
/**
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* is attached to the ExpressionStatement itself rather than the nested identifier.
|
|
996
|
+
* Gets the explanation for why the match failed.
|
|
997
|
+
* Part of Layer 2 (Public API).
|
|
628
998
|
*
|
|
629
|
-
* @
|
|
630
|
-
* @param visited Set of already visited nodes to avoid cycles
|
|
999
|
+
* @returns The match explanation, or undefined if match succeeded or no explanation available
|
|
631
1000
|
*/
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Mark as visited to avoid cycles
|
|
638
|
-
visited.add(node);
|
|
639
|
-
|
|
640
|
-
// Check if this is an ExpressionStatement containing a capture identifier
|
|
641
|
-
// For statement-level captures, we attach the marker to the ExpressionStatement itself
|
|
642
|
-
if (node.kind === JS.Kind.ExpressionStatement &&
|
|
643
|
-
node.expression?.kind === J.Kind.Identifier &&
|
|
644
|
-
node.expression.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
|
|
645
|
-
|
|
646
|
-
const captureInfo = PlaceholderUtils.parseCapture(node.expression.simpleName);
|
|
647
|
-
if (captureInfo) {
|
|
648
|
-
// Initialize markers on the ExpressionStatement
|
|
649
|
-
if (!node.markers) {
|
|
650
|
-
node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
|
|
651
|
-
}
|
|
652
|
-
if (!node.markers.markers) {
|
|
653
|
-
node.markers.markers = [];
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Find the original capture object to get variadic options
|
|
657
|
-
const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
|
|
658
|
-
const variadicOptions = captureObj?.getVariadicOptions();
|
|
659
|
-
|
|
660
|
-
// Add CaptureMarker to the ExpressionStatement
|
|
661
|
-
node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// For non-statement captures (expressions), attach marker to the identifier
|
|
665
|
-
else if (node.kind === J.Kind.Identifier && node.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
|
|
666
|
-
const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
|
|
667
|
-
if (captureInfo) {
|
|
668
|
-
// Initialize markers if needed
|
|
669
|
-
if (!node.markers) {
|
|
670
|
-
node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
|
|
671
|
-
}
|
|
672
|
-
if (!node.markers.markers) {
|
|
673
|
-
node.markers.markers = [];
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Find the original capture object to get variadic options
|
|
677
|
-
const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
|
|
678
|
-
const variadicOptions = captureObj?.getVariadicOptions();
|
|
679
|
-
|
|
680
|
-
// Add CaptureMarker with variadic options if available
|
|
681
|
-
node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Recursively visit all properties
|
|
686
|
-
for (const key in node) {
|
|
687
|
-
if (node.hasOwnProperty(key)) {
|
|
688
|
-
const value = node[key];
|
|
689
|
-
if (Array.isArray(value)) {
|
|
690
|
-
value.forEach(item => this.visitAndAttachMarkers(item, visited));
|
|
691
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
692
|
-
this.visitAndAttachMarkers(value, visited);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
1001
|
+
getExplanation(): MatchExplanation | undefined {
|
|
1002
|
+
return this.explanation;
|
|
696
1003
|
}
|
|
697
1004
|
}
|
|
698
1005
|
|
|
@@ -700,7 +1007,7 @@ class TemplateProcessor {
|
|
|
700
1007
|
* Tagged template function for creating patterns.
|
|
701
1008
|
*
|
|
702
1009
|
* @param strings The string parts of the template
|
|
703
|
-
* @param captures The captures between the string parts (Capture, Any, or string names)
|
|
1010
|
+
* @param captures The captures between the string parts (Capture, Any, RawCode, or string names)
|
|
704
1011
|
* @returns A Pattern object
|
|
705
1012
|
*
|
|
706
1013
|
* @example
|
|
@@ -711,17 +1018,84 @@ class TemplateProcessor {
|
|
|
711
1018
|
* @example
|
|
712
1019
|
* // Using any() for non-capturing matches
|
|
713
1020
|
* const pat = pattern`foo(${any()})`;
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* // Using raw() for dynamic pattern construction
|
|
1024
|
+
* const operator = '===';
|
|
1025
|
+
* const pat = pattern`x ${raw(operator)} y`;
|
|
1026
|
+
*/
|
|
1027
|
+
/**
|
|
1028
|
+
* Creates a pattern from a template literal (direct usage).
|
|
1029
|
+
*
|
|
1030
|
+
* @example
|
|
1031
|
+
* ```typescript
|
|
1032
|
+
* const pat = pattern`console.log(${x})`;
|
|
1033
|
+
* ```
|
|
714
1034
|
*/
|
|
715
|
-
export function pattern(strings: TemplateStringsArray, ...captures: (Capture | Any<any> | string)[]): Pattern
|
|
1035
|
+
export function pattern(strings: TemplateStringsArray, ...captures: (Capture | Any<any> | RawCode | string)[]): Pattern;
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Creates a pattern factory with options that returns a tagged template function.
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```typescript
|
|
1042
|
+
* const pat = pattern({ debug: true })`console.log(${x})`;
|
|
1043
|
+
* ```
|
|
1044
|
+
*/
|
|
1045
|
+
export function pattern(options: PatternOptions): (strings: TemplateStringsArray, ...captures: (Capture | Any<any> | RawCode | string)[]) => Pattern;
|
|
1046
|
+
|
|
1047
|
+
// Implementation
|
|
1048
|
+
export function pattern(
|
|
1049
|
+
stringsOrOptions: TemplateStringsArray | PatternOptions,
|
|
1050
|
+
...captures: (Capture | Any<any> | RawCode | string)[]
|
|
1051
|
+
): Pattern | ((strings: TemplateStringsArray, ...captures: (Capture | Any<any> | RawCode | string)[]) => Pattern) {
|
|
1052
|
+
// Check if first arg is TemplateStringsArray (direct usage)
|
|
1053
|
+
if (Array.isArray(stringsOrOptions) && 'raw' in stringsOrOptions) {
|
|
1054
|
+
// Direct usage: pattern`...`
|
|
1055
|
+
return createPattern(stringsOrOptions as TemplateStringsArray, captures, {});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Options usage: pattern({ ... })`...`
|
|
1059
|
+
const options = stringsOrOptions as PatternOptions;
|
|
1060
|
+
return (strings: TemplateStringsArray, ...caps: (Capture | Any<any> | RawCode | string)[]): Pattern => {
|
|
1061
|
+
return createPattern(strings, caps, options);
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Internal helper to create a Pattern instance.
|
|
1067
|
+
* @private
|
|
1068
|
+
*/
|
|
1069
|
+
function createPattern(
|
|
1070
|
+
strings: TemplateStringsArray,
|
|
1071
|
+
captures: (Capture | Any<any> | RawCode | string)[],
|
|
1072
|
+
options: PatternOptions
|
|
1073
|
+
): Pattern {
|
|
716
1074
|
const capturesByName = captures.reduce((map, c) => {
|
|
1075
|
+
// Skip raw code - it's not a capture
|
|
1076
|
+
if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) {
|
|
1077
|
+
return map;
|
|
1078
|
+
}
|
|
717
1079
|
const capture = typeof c === "string" ? new CaptureImpl(c) : c;
|
|
718
1080
|
// Use symbol to get internal name without triggering Proxy
|
|
719
1081
|
const name = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
720
1082
|
return map.set(name, capture);
|
|
721
1083
|
}, new Map<string, Capture | Any<any>>());
|
|
722
|
-
|
|
1084
|
+
|
|
1085
|
+
const pat = new Pattern(strings, captures.map(c => {
|
|
1086
|
+
// Return raw code as-is
|
|
1087
|
+
if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) {
|
|
1088
|
+
return c as RawCode;
|
|
1089
|
+
}
|
|
723
1090
|
// Use symbol to get internal name without triggering Proxy
|
|
724
1091
|
const name = typeof c === "string" ? c : ((c as any)[CAPTURE_NAME_SYMBOL] || c.getName());
|
|
725
1092
|
return capturesByName.get(name)!;
|
|
726
1093
|
}));
|
|
1094
|
+
|
|
1095
|
+
// Apply options if provided
|
|
1096
|
+
if (options && Object.keys(options).length > 0) {
|
|
1097
|
+
pat.configure(options);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return pat;
|
|
727
1101
|
}
|