@openrewrite/rewrite 8.67.0-20251119-160338 → 8.67.0-20251120-075051
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/javascript/add-import.d.ts +21 -10
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +55 -21
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/remove-import.d.ts +18 -14
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +37 -47
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/javascript/templating/capture.d.ts +3 -4
- package/dist/javascript/templating/capture.d.ts.map +1 -1
- package/dist/javascript/templating/capture.js +3 -3
- package/dist/javascript/templating/capture.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts +7 -1
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +45 -10
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +10 -11
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +18 -36
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/rewrite.js +2 -2
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +27 -13
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +31 -14
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +111 -15
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/add-import.ts +70 -27
- package/src/javascript/remove-import.ts +37 -46
- package/src/javascript/templating/capture.ts +7 -7
- package/src/javascript/templating/comparator.ts +50 -11
- package/src/javascript/templating/pattern.ts +32 -24
- package/src/javascript/templating/rewrite.ts +2 -2
- package/src/javascript/templating/template.ts +36 -18
- package/src/javascript/templating/types.ts +127 -16
|
@@ -6,16 +6,16 @@ import {ElementRemovalFormatter} from "../java/formatting-utils";
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param visitor The visitor to add the import removal to
|
|
9
|
-
* @param
|
|
10
|
-
* or the name of the import to remove entirely
|
|
9
|
+
* @param module The module name (e.g., 'fs', 'react') to remove imports from
|
|
11
10
|
* @param member Optionally, the specific member to remove from the import.
|
|
12
|
-
* If not specified, removes
|
|
11
|
+
* If not specified, removes all unused imports from the module.
|
|
13
12
|
* Special values:
|
|
14
|
-
* - 'default': Removes the default import from the
|
|
13
|
+
* - 'default': Removes the default import from the module if unused,
|
|
15
14
|
* regardless of its local name (e.g., `import React from 'react'`)
|
|
15
|
+
* - '*': Removes the namespace import if unused (e.g., `import * as fs from 'fs'`)
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
|
-
* // Remove a named import if unused
|
|
18
|
+
* // Remove a specific named import if unused
|
|
19
19
|
* maybeRemoveImport(visitor, 'fs', 'readFile');
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
@@ -23,16 +23,20 @@ import {ElementRemovalFormatter} from "../java/formatting-utils";
|
|
|
23
23
|
* maybeRemoveImport(visitor, 'react', 'default');
|
|
24
24
|
*
|
|
25
25
|
* @example
|
|
26
|
-
* // Remove
|
|
27
|
-
* maybeRemoveImport(visitor, '
|
|
26
|
+
* // Remove all unused imports from 'react' module
|
|
27
|
+
* maybeRemoveImport(visitor, 'react');
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Remove namespace import if unused
|
|
31
|
+
* maybeRemoveImport(visitor, 'fs', '*');
|
|
28
32
|
*/
|
|
29
|
-
export function maybeRemoveImport(visitor: JavaScriptVisitor<any>,
|
|
33
|
+
export function maybeRemoveImport(visitor: JavaScriptVisitor<any>, module: string, member?: string) {
|
|
30
34
|
for (const v of visitor.afterVisit || []) {
|
|
31
|
-
if (v instanceof RemoveImport && v.
|
|
35
|
+
if (v instanceof RemoveImport && v.module === module && v.member === member) {
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
|
-
visitor.afterVisit.push(new RemoveImport(
|
|
39
|
+
visitor.afterVisit.push(new RemoveImport(module, member));
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
// Type alias for RightPadded elements to simplify type signatures
|
|
@@ -45,15 +49,15 @@ type RightPaddedElement<T extends J> = {
|
|
|
45
49
|
|
|
46
50
|
export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
47
51
|
/**
|
|
48
|
-
* @param
|
|
49
|
-
* or the name of the import to remove entirely
|
|
52
|
+
* @param module The module name (e.g., 'fs', 'react') to remove imports from
|
|
50
53
|
* @param member Optionally, the specific member to remove from the import.
|
|
51
|
-
* If not specified, removes
|
|
54
|
+
* If not specified, removes all unused imports from the module.
|
|
52
55
|
* Special values:
|
|
53
|
-
* - 'default': Removes the default import from the
|
|
56
|
+
* - 'default': Removes the default import from the module if unused,
|
|
54
57
|
* regardless of its local name
|
|
58
|
+
* - '*': Removes the namespace import if unused
|
|
55
59
|
*/
|
|
56
|
-
constructor(readonly
|
|
60
|
+
constructor(readonly module: string,
|
|
57
61
|
readonly member?: string) {
|
|
58
62
|
super();
|
|
59
63
|
}
|
|
@@ -245,7 +249,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
245
249
|
const name = identifier.simpleName;
|
|
246
250
|
|
|
247
251
|
// Check if we should remove this default import
|
|
248
|
-
let shouldRemove
|
|
252
|
+
let shouldRemove: boolean;
|
|
249
253
|
if (this.member === 'default') {
|
|
250
254
|
// Special case: member 'default' means remove any default import from the target module if unused
|
|
251
255
|
shouldRemove = !usedIdentifiers.has(name) && !usedTypes.has(name);
|
|
@@ -425,22 +429,17 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
425
429
|
* Check if the module name matches the target module
|
|
426
430
|
*/
|
|
427
431
|
private matchesTargetModule(moduleName: string): boolean {
|
|
428
|
-
return
|
|
432
|
+
return moduleName === this.module;
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
/**
|
|
432
436
|
* Check if an identifier should be removed based on usage
|
|
433
437
|
*/
|
|
434
438
|
private shouldRemoveIdentifier(name: string, usedIdentifiers: Set<string>, usedTypes: Set<string>): boolean {
|
|
435
|
-
//
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
} else {
|
|
440
|
-
// We're removing based on the target name
|
|
441
|
-
// Check if the name matches and is not used
|
|
442
|
-
return this.target === name && !usedIdentifiers.has(name) && !usedTypes.has(name);
|
|
443
|
-
}
|
|
439
|
+
// For CommonJS and import-equals-require, we're removing the entire import
|
|
440
|
+
// if the identifier is not used (member is typically undefined for these cases,
|
|
441
|
+
// or we're checking if a specific binding is used)
|
|
442
|
+
return !usedIdentifiers.has(name) && !usedTypes.has(name);
|
|
444
443
|
}
|
|
445
444
|
|
|
446
445
|
private async processNamedImports(
|
|
@@ -681,40 +680,32 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
681
680
|
usedIdentifiers: Set<string>,
|
|
682
681
|
usedTypes: Set<string>
|
|
683
682
|
): boolean {
|
|
684
|
-
// If member is specified, we're removing a specific member from
|
|
683
|
+
// If member is specified, we're removing a specific member from the module
|
|
685
684
|
if (this.member !== undefined) {
|
|
686
685
|
// Only remove if this is the specific member we're looking for
|
|
687
686
|
if (this.member !== name) {
|
|
688
687
|
return false;
|
|
689
688
|
}
|
|
690
|
-
} else {
|
|
691
|
-
// If no member specified, we're removing based on the import name itself
|
|
692
|
-
if (this.target !== name) {
|
|
693
|
-
return false;
|
|
694
|
-
}
|
|
695
689
|
}
|
|
690
|
+
// If no member specified, we're removing all unused imports from the module
|
|
691
|
+
// So we check if this particular import is unused
|
|
696
692
|
|
|
697
693
|
// Check if it's used
|
|
698
694
|
return !(usedIdentifiers.has(name) || usedTypes.has(name));
|
|
699
695
|
}
|
|
700
696
|
|
|
701
697
|
private isTargetModule(jsImport: JS.Import): boolean {
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return false;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const literal = moduleSpecifier as J.Literal;
|
|
710
|
-
const moduleName = literal.value?.toString().replace(/['"`]/g, '');
|
|
711
|
-
|
|
712
|
-
// Match the module name
|
|
713
|
-
return moduleName === this.target;
|
|
698
|
+
// Always check if the import is from the specified module
|
|
699
|
+
const moduleSpecifier = jsImport.moduleSpecifier?.element;
|
|
700
|
+
if (!moduleSpecifier || moduleSpecifier.kind !== J.Kind.Literal) {
|
|
701
|
+
return false;
|
|
714
702
|
}
|
|
715
703
|
|
|
716
|
-
|
|
717
|
-
|
|
704
|
+
const literal = moduleSpecifier as J.Literal;
|
|
705
|
+
const moduleName = literal.value?.toString().replace(/['"`]/g, '');
|
|
706
|
+
|
|
707
|
+
// Match the module name
|
|
708
|
+
return moduleName === this.module;
|
|
718
709
|
}
|
|
719
710
|
|
|
720
711
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {Cursor} from '../..';
|
|
17
17
|
import {J, Type} from '../../java';
|
|
18
|
-
import {Any, Capture, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types';
|
|
18
|
+
import {Any, Capture, CaptureConstraintContext, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Combines multiple constraints with AND logic.
|
|
@@ -30,8 +30,8 @@ import {Any, Capture, CaptureOptions, ConstraintFunction, TemplateParam, Variadi
|
|
|
30
30
|
* )
|
|
31
31
|
* });
|
|
32
32
|
*/
|
|
33
|
-
export function and<T>(...constraints:
|
|
34
|
-
return (node: T,
|
|
33
|
+
export function and<T>(...constraints: ConstraintFunction<T>[]): ConstraintFunction<T> {
|
|
34
|
+
return (node: T, context: CaptureConstraintContext) => constraints.every(c => c(node, context));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -46,8 +46,8 @@ export function and<T>(...constraints: ((node: T, cursor?: Cursor) => boolean)[]
|
|
|
46
46
|
* )
|
|
47
47
|
* });
|
|
48
48
|
*/
|
|
49
|
-
export function or<T>(...constraints:
|
|
50
|
-
return (node: T,
|
|
49
|
+
export function or<T>(...constraints: ConstraintFunction<T>[]): ConstraintFunction<T> {
|
|
50
|
+
return (node: T, context: CaptureConstraintContext) => constraints.some(c => c(node, context));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -59,8 +59,8 @@ export function or<T>(...constraints: ((node: T, cursor?: Cursor) => boolean)[])
|
|
|
59
59
|
* constraint: not((node) => typeof node.value === 'string')
|
|
60
60
|
* });
|
|
61
61
|
*/
|
|
62
|
-
export function not<T>(constraint:
|
|
63
|
-
return (node: T,
|
|
62
|
+
export function not<T>(constraint: ConstraintFunction<T>): ConstraintFunction<T> {
|
|
63
|
+
return (node: T, context: CaptureConstraintContext) => !constraint(node, context);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Symbol to access the internal capture name without triggering Proxy
|
|
@@ -18,7 +18,8 @@ import {J} from '../../java';
|
|
|
18
18
|
import {JS} from '../index';
|
|
19
19
|
import {JavaScriptSemanticComparatorVisitor} from '../comparator';
|
|
20
20
|
import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils';
|
|
21
|
-
import {DebugLogEntry, MatchExplanation} from './types';
|
|
21
|
+
import {Capture, CaptureConstraintContext, CaptureMap, DebugLogEntry, MatchExplanation} from './types';
|
|
22
|
+
import {CAPTURE_NAME_SYMBOL} from './capture';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Debug callbacks for pattern matching.
|
|
@@ -62,6 +63,28 @@ export interface MatcherCallbacks {
|
|
|
62
63
|
debug?: DebugCallbacks;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Implementation of CaptureMap that wraps the capture storage.
|
|
68
|
+
* Provides read-only access to previously matched captures.
|
|
69
|
+
*/
|
|
70
|
+
class CaptureMapImpl implements CaptureMap {
|
|
71
|
+
constructor(private readonly storage: Map<string, CaptureStorageValue>) {}
|
|
72
|
+
|
|
73
|
+
get<T>(capture: Capture<T>): T | undefined;
|
|
74
|
+
get(capture: string): any;
|
|
75
|
+
get(capture: Capture | string): any {
|
|
76
|
+
// Use symbol to get internal name without triggering Proxy
|
|
77
|
+
const name = typeof capture === 'string' ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
|
|
78
|
+
return this.storage.get(name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
has(capture: Capture | string): boolean {
|
|
82
|
+
// Use symbol to get internal name without triggering Proxy
|
|
83
|
+
const name = typeof capture === 'string' ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
|
|
84
|
+
return this.storage.has(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
/**
|
|
66
89
|
* A comparator for pattern matching that is lenient about optional properties.
|
|
67
90
|
* Allows patterns without type annotations to match actual code with type annotations.
|
|
@@ -76,6 +99,19 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
76
99
|
super(lenientTypeMatching);
|
|
77
100
|
}
|
|
78
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Builds the constraint context with the cursor and current captures.
|
|
104
|
+
* @param cursor The cursor to include in the context
|
|
105
|
+
* @returns The constraint context for evaluating capture constraints
|
|
106
|
+
*/
|
|
107
|
+
protected buildConstraintContext(cursor: Cursor): CaptureConstraintContext {
|
|
108
|
+
const state = this.matcher.saveState();
|
|
109
|
+
return {
|
|
110
|
+
cursor,
|
|
111
|
+
captures: new CaptureMapImpl(state.storage)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
79
115
|
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
80
116
|
// Check if the pattern node is a capture - this handles unwrapped captures
|
|
81
117
|
// (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
|
|
@@ -91,12 +127,15 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
91
127
|
: new Cursor(p);
|
|
92
128
|
this.targetCursor = cursorAtCapturedNode;
|
|
93
129
|
try {
|
|
94
|
-
// Evaluate constraint with cursor
|
|
130
|
+
// Evaluate constraint with context (cursor + previous captures)
|
|
95
131
|
// Skip constraint for variadic captures - they're evaluated in matchSequence with the full array
|
|
96
|
-
if (captureMarker.constraint && !captureMarker.variadicOptions
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
if (captureMarker.constraint && !captureMarker.variadicOptions) {
|
|
133
|
+
const context = this.buildConstraintContext(cursorAtCapturedNode);
|
|
134
|
+
if (!captureMarker.constraint(p, context)) {
|
|
135
|
+
const captureName = captureMarker.captureName || 'unnamed';
|
|
136
|
+
const targetKind = (p as any).kind || 'unknown';
|
|
137
|
+
return this.constraintFailed(captureName, targetKind) as R;
|
|
138
|
+
}
|
|
100
139
|
}
|
|
101
140
|
|
|
102
141
|
const success = this.matcher.handleCapture(captureMarker, p, undefined);
|
|
@@ -164,7 +203,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
164
203
|
try {
|
|
165
204
|
// Evaluate constraint with cursor at the captured node (always defined)
|
|
166
205
|
// Skip constraint for variadic captures - they're evaluated in matchSequence with the full array
|
|
167
|
-
if (captureMarker.constraint && !captureMarker.variadicOptions && !captureMarker.constraint(targetElement as J, cursorAtCapturedNode)) {
|
|
206
|
+
if (captureMarker.constraint && !captureMarker.variadicOptions && !captureMarker.constraint(targetElement as J, this.buildConstraintContext(cursorAtCapturedNode))) {
|
|
168
207
|
const captureName = captureMarker.captureName || 'unnamed';
|
|
169
208
|
const targetKind = (targetElement as any).kind || 'unknown';
|
|
170
209
|
return this.constraintFailed(captureName, targetKind);
|
|
@@ -604,7 +643,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
604
643
|
// The targetCursor points to the parent container (always defined in container matching)
|
|
605
644
|
if (captureMarker.constraint) {
|
|
606
645
|
const cursor = this.targetCursor || new Cursor(targetElements[0]);
|
|
607
|
-
if (!captureMarker.constraint(capturedElements as any, cursor)) {
|
|
646
|
+
if (!captureMarker.constraint(capturedElements as any, this.buildConstraintContext(cursor))) {
|
|
608
647
|
continue; // Try next consumption amount
|
|
609
648
|
}
|
|
610
649
|
}
|
|
@@ -868,7 +907,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
|
|
|
868
907
|
try {
|
|
869
908
|
if (captureMarker.constraint && !captureMarker.variadicOptions) {
|
|
870
909
|
this.debug.log('debug', 'constraint', `Evaluating constraint for capture: ${captureMarker.captureName}`);
|
|
871
|
-
const constraintResult = captureMarker.constraint(p, cursorAtCapturedNode);
|
|
910
|
+
const constraintResult = captureMarker.constraint(p, this.buildConstraintContext(cursorAtCapturedNode));
|
|
872
911
|
if (!constraintResult) {
|
|
873
912
|
this.debug.log('info', 'constraint', `Constraint failed for capture: ${captureMarker.captureName}`);
|
|
874
913
|
this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(p as any).kind}`, `Constraint evaluation returned false`);
|
|
@@ -965,7 +1004,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
|
|
|
965
1004
|
try {
|
|
966
1005
|
if (captureMarker.constraint && !captureMarker.variadicOptions) {
|
|
967
1006
|
this.debug.log('debug', 'constraint', `Evaluating constraint for wrapped capture: ${captureMarker.captureName}`);
|
|
968
|
-
const constraintResult = captureMarker.constraint(targetElement as J, cursorAtCapturedNode);
|
|
1007
|
+
const constraintResult = captureMarker.constraint(targetElement as J, this.buildConstraintContext(cursorAtCapturedNode));
|
|
969
1008
|
if (!constraintResult) {
|
|
970
1009
|
this.debug.log('info', 'constraint', `Constraint failed for wrapped capture: ${captureMarker.captureName}`);
|
|
971
1010
|
this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(targetElement as any).kind}`, `Constraint evaluation returned false`);
|
|
@@ -1316,7 +1355,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
|
|
|
1316
1355
|
if (captureMarker.constraint) {
|
|
1317
1356
|
this.debug.log('debug', 'constraint', `Evaluating variadic constraint for capture: ${captureMarker.captureName} (${capturedElements.length} elements)`);
|
|
1318
1357
|
const cursor = this.targetCursor || new Cursor(targetElements[0]);
|
|
1319
|
-
const constraintResult = captureMarker.constraint(capturedElements as any, cursor);
|
|
1358
|
+
const constraintResult = captureMarker.constraint(capturedElements as any, this.buildConstraintContext(cursor));
|
|
1320
1359
|
if (!constraintResult) {
|
|
1321
1360
|
this.debug.log('info', 'constraint', `Variadic constraint failed for capture: ${captureMarker.captureName}`);
|
|
1322
1361
|
continue;
|
|
@@ -15,7 +15,17 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {Cursor} from '../..';
|
|
17
17
|
import {J} from '../../java';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
Any,
|
|
20
|
+
Capture,
|
|
21
|
+
DebugLogEntry,
|
|
22
|
+
DebugOptions,
|
|
23
|
+
MatchAttemptResult,
|
|
24
|
+
MatchExplanation,
|
|
25
|
+
MatchOptions,
|
|
26
|
+
MatchResult as IMatchResult,
|
|
27
|
+
PatternOptions
|
|
28
|
+
} from './types';
|
|
19
29
|
import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture';
|
|
20
30
|
import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator';
|
|
21
31
|
import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
@@ -190,7 +200,7 @@ export class Pattern {
|
|
|
190
200
|
* })
|
|
191
201
|
*/
|
|
192
202
|
configure(options: PatternOptions): Pattern {
|
|
193
|
-
this._options = {...this._options, ...options};
|
|
203
|
+
this._options = { ...this._options, ...options };
|
|
194
204
|
// Invalidate cache when configuration changes
|
|
195
205
|
this._cachedAstPattern = undefined;
|
|
196
206
|
return this;
|
|
@@ -252,23 +262,22 @@ export class Pattern {
|
|
|
252
262
|
/**
|
|
253
263
|
* Creates a matcher for this pattern against a specific AST node.
|
|
254
264
|
*
|
|
255
|
-
* @param
|
|
256
|
-
* @param cursor
|
|
257
|
-
* capture constraints to navigate to parent nodes.
|
|
258
|
-
* created at the ast root, allowing constraints to navigate within the matched subtree.
|
|
265
|
+
* @param tree The AST node to match against
|
|
266
|
+
* @param cursor Cursor at the node's position in a larger tree. Used for context-aware
|
|
267
|
+
* capture constraints to navigate to parent nodes.
|
|
259
268
|
* @param options Optional match options (e.g., debug flag)
|
|
260
269
|
* @returns A MatchResult if the pattern matches, undefined otherwise
|
|
261
270
|
*
|
|
262
271
|
* @example
|
|
263
272
|
* ```typescript
|
|
264
273
|
* // Normal match
|
|
265
|
-
* const match = await pattern.match(node);
|
|
274
|
+
* const match = await pattern.match(node, cursor);
|
|
266
275
|
*
|
|
267
276
|
* // Debug this specific call
|
|
268
277
|
* const match = await pattern.match(node, cursor, { debug: true });
|
|
269
278
|
* ```
|
|
270
279
|
*/
|
|
271
|
-
async match(
|
|
280
|
+
async match(tree: J, cursor: Cursor, options?: MatchOptions): Promise<MatchResult | undefined> {
|
|
272
281
|
// Three-level precedence: call > pattern > global
|
|
273
282
|
const debugEnabled =
|
|
274
283
|
options?.debug !== undefined
|
|
@@ -279,8 +288,8 @@ export class Pattern {
|
|
|
279
288
|
|
|
280
289
|
if (debugEnabled) {
|
|
281
290
|
// Use matchWithExplanation and log the result
|
|
282
|
-
const result = await this.matchWithExplanation(
|
|
283
|
-
await this.logMatchResult(
|
|
291
|
+
const result = await this.matchWithExplanation(tree, cursor);
|
|
292
|
+
await this.logMatchResult(tree, cursor, result);
|
|
284
293
|
|
|
285
294
|
if (result.matched) {
|
|
286
295
|
// result.result is the MatchResult class instance
|
|
@@ -291,7 +300,7 @@ export class Pattern {
|
|
|
291
300
|
}
|
|
292
301
|
|
|
293
302
|
// Fast path - no debug
|
|
294
|
-
const matcher = new Matcher(this,
|
|
303
|
+
const matcher = new Matcher(this, tree, cursor);
|
|
295
304
|
const success = await matcher.matches();
|
|
296
305
|
if (!success) {
|
|
297
306
|
return undefined;
|
|
@@ -305,10 +314,10 @@ export class Pattern {
|
|
|
305
314
|
* Formats and logs the match result to stderr.
|
|
306
315
|
* @private
|
|
307
316
|
*/
|
|
308
|
-
private async logMatchResult(
|
|
317
|
+
private async logMatchResult(tree: J, cursor: Cursor | undefined, result: MatchAttemptResult): Promise<void> {
|
|
309
318
|
const patternSource = this.getPatternSource();
|
|
310
319
|
const patternId = `Pattern #${this.patternId}`;
|
|
311
|
-
const nodeKind = (
|
|
320
|
+
const nodeKind = (tree as any).kind || 'unknown';
|
|
312
321
|
// Format kind: extract short name (e.g., "org.openrewrite.java.tree.J$Binary" -> "J$Binary")
|
|
313
322
|
const shortKind = typeof nodeKind === 'string'
|
|
314
323
|
? nodeKind.split('.').pop() || nodeKind
|
|
@@ -324,7 +333,7 @@ export class Pattern {
|
|
|
324
333
|
let treeStr: string;
|
|
325
334
|
try {
|
|
326
335
|
const printer = TreePrinters.printer(JS.Kind.CompilationUnit);
|
|
327
|
-
treeStr = await printer.print(
|
|
336
|
+
treeStr = await printer.print(tree);
|
|
328
337
|
} catch (e) {
|
|
329
338
|
treeStr = '(tree printing unavailable)';
|
|
330
339
|
}
|
|
@@ -508,15 +517,15 @@ export class Pattern {
|
|
|
508
517
|
* - Explanation of failure (if not matched)
|
|
509
518
|
* - Debug log entries showing the matching process
|
|
510
519
|
*
|
|
511
|
-
* @param
|
|
512
|
-
* @param cursor
|
|
520
|
+
* @param tree The AST node to match against
|
|
521
|
+
* @param cursor Cursor at the node's position in a larger tree
|
|
513
522
|
* @param debugOptions Optional debug options (defaults to all logging enabled)
|
|
514
523
|
* @returns Detailed result with debug information
|
|
515
524
|
*
|
|
516
525
|
* @example
|
|
517
526
|
* const x = capture('x');
|
|
518
527
|
* const pat = pattern`console.log(${x})`;
|
|
519
|
-
* const attempt = await pat.matchWithExplanation(node);
|
|
528
|
+
* const attempt = await pat.matchWithExplanation(node, cursor);
|
|
520
529
|
* if (attempt.matched) {
|
|
521
530
|
* console.log('Matched!');
|
|
522
531
|
* console.log('Captured x:', attempt.result.get('x'));
|
|
@@ -526,8 +535,8 @@ export class Pattern {
|
|
|
526
535
|
* }
|
|
527
536
|
*/
|
|
528
537
|
async matchWithExplanation(
|
|
529
|
-
|
|
530
|
-
cursor
|
|
538
|
+
tree: J,
|
|
539
|
+
cursor: Cursor,
|
|
531
540
|
debugOptions?: DebugOptions
|
|
532
541
|
): Promise<MatchAttemptResult> {
|
|
533
542
|
// Default to full debug logging if not specified
|
|
@@ -538,7 +547,7 @@ export class Pattern {
|
|
|
538
547
|
...debugOptions
|
|
539
548
|
};
|
|
540
549
|
|
|
541
|
-
const matcher = new Matcher(this,
|
|
550
|
+
const matcher = new Matcher(this, tree, cursor, options);
|
|
542
551
|
const success = await matcher.matches();
|
|
543
552
|
|
|
544
553
|
if (success) {
|
|
@@ -671,17 +680,16 @@ class Matcher {
|
|
|
671
680
|
*
|
|
672
681
|
* @param pattern The pattern to match
|
|
673
682
|
* @param ast The AST node to match against
|
|
674
|
-
* @param cursor
|
|
683
|
+
* @param cursor Cursor at the AST node's position
|
|
675
684
|
* @param debugOptions Optional debug options for instrumentation
|
|
676
685
|
*/
|
|
677
686
|
constructor(
|
|
678
687
|
private readonly pattern: Pattern,
|
|
679
688
|
private readonly ast: J,
|
|
680
|
-
cursor
|
|
689
|
+
cursor: Cursor,
|
|
681
690
|
debugOptions?: DebugOptions
|
|
682
691
|
) {
|
|
683
|
-
|
|
684
|
-
this.cursor = cursor ?? new Cursor(ast, undefined);
|
|
692
|
+
this.cursor = cursor;
|
|
685
693
|
this.debugOptions = debugOptions ?? {};
|
|
686
694
|
}
|
|
687
695
|
|
|
@@ -57,10 +57,10 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
57
57
|
if (typeof this.after === 'function') {
|
|
58
58
|
// Call the function to get a template, then apply it
|
|
59
59
|
const template = this.after(match);
|
|
60
|
-
result = await template.apply(
|
|
60
|
+
result = await template.apply(node, cursor, { values: match });
|
|
61
61
|
} else {
|
|
62
62
|
// Use template.apply() as before
|
|
63
|
-
result = await this.after.apply(
|
|
63
|
+
result = await this.after.apply(node, cursor, { values: match });
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
if (result) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {Cursor, Tree} from '../..';
|
|
17
17
|
import {J} from '../../java';
|
|
18
|
-
import {
|
|
18
|
+
import {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types';
|
|
19
19
|
import {MatchResult} from './pattern';
|
|
20
20
|
import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
21
21
|
import {CAPTURE_NAME_SYMBOL, RAW_CODE_SYMBOL} from './capture';
|
|
@@ -174,6 +174,18 @@ export class Template {
|
|
|
174
174
|
private options: TemplateOptions = {};
|
|
175
175
|
private _cachedTemplate?: J;
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Creates a new template.
|
|
179
|
+
*
|
|
180
|
+
* @param templateParts The string parts of the template
|
|
181
|
+
* @param parameters The parameters between the string parts
|
|
182
|
+
*/
|
|
183
|
+
constructor(
|
|
184
|
+
private readonly templateParts: TemplateStringsArray,
|
|
185
|
+
private readonly parameters: Parameter[]
|
|
186
|
+
) {
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
/**
|
|
178
190
|
* Creates a new builder for constructing templates programmatically.
|
|
179
191
|
*
|
|
@@ -191,18 +203,6 @@ export class Template {
|
|
|
191
203
|
return new TemplateBuilder();
|
|
192
204
|
}
|
|
193
205
|
|
|
194
|
-
/**
|
|
195
|
-
* Creates a new template.
|
|
196
|
-
*
|
|
197
|
-
* @param templateParts The string parts of the template
|
|
198
|
-
* @param parameters The parameters between the string parts
|
|
199
|
-
*/
|
|
200
|
-
constructor(
|
|
201
|
-
private readonly templateParts: TemplateStringsArray,
|
|
202
|
-
private readonly parameters: Parameter[]
|
|
203
|
-
) {
|
|
204
|
-
}
|
|
205
|
-
|
|
206
206
|
/**
|
|
207
207
|
* Configures this template with additional options.
|
|
208
208
|
*
|
|
@@ -217,7 +217,7 @@ export class Template {
|
|
|
217
217
|
* })
|
|
218
218
|
*/
|
|
219
219
|
configure(options: TemplateOptions): Template {
|
|
220
|
-
this.options = {
|
|
220
|
+
this.options = {...this.options, ...options};
|
|
221
221
|
// Invalidate cache when configuration changes
|
|
222
222
|
this._cachedTemplate = undefined;
|
|
223
223
|
return this;
|
|
@@ -236,7 +236,7 @@ export class Template {
|
|
|
236
236
|
* @returns The cached or newly computed template tree
|
|
237
237
|
* @internal
|
|
238
238
|
*/
|
|
239
|
-
async getTemplateTree(): Promise<JS.CompilationUnit> {
|
|
239
|
+
private async getTemplateTree(): Promise<JS.CompilationUnit> {
|
|
240
240
|
// Level 1: Instance cache (fastest path)
|
|
241
241
|
if (this._cachedTemplate) {
|
|
242
242
|
return this._cachedTemplate as JS.CompilationUnit;
|
|
@@ -286,12 +286,30 @@ export class Template {
|
|
|
286
286
|
/**
|
|
287
287
|
* Applies this template and returns the resulting tree.
|
|
288
288
|
*
|
|
289
|
+
* @param tree Input tree to transform
|
|
289
290
|
* @param cursor The cursor pointing to the current location in the AST
|
|
290
|
-
* @param
|
|
291
|
-
* @param values values for parameters in template
|
|
291
|
+
* @param options Optional configuration including values for parameters
|
|
292
292
|
* @returns A Promise resolving to the generated AST node
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* // Simple application without values
|
|
297
|
+
* const result = await tmpl.apply(node, cursor);
|
|
298
|
+
*
|
|
299
|
+
* // With values from pattern match
|
|
300
|
+
* const match = await pat.match(node, cursor);
|
|
301
|
+
* const result = await tmpl.apply(node, cursor, { values: match });
|
|
302
|
+
*
|
|
303
|
+
* // With explicit values
|
|
304
|
+
* const result = await tmpl.apply(node, cursor, {
|
|
305
|
+
* values: { x: someNode, y: anotherNode }
|
|
306
|
+
* });
|
|
307
|
+
* ```
|
|
293
308
|
*/
|
|
294
|
-
async apply(
|
|
309
|
+
async apply(tree: J, cursor: Cursor, options?: ApplyOptions): Promise<J | undefined> {
|
|
310
|
+
// Extract values from options
|
|
311
|
+
const values = options?.values;
|
|
312
|
+
|
|
295
313
|
// Normalize the values map: convert any Capture keys to string keys
|
|
296
314
|
let normalizedValues: Pick<Map<string, J>, 'get'> | undefined;
|
|
297
315
|
let wrappersMap: Map<string, J.RightPadded<J> | J.RightPadded<J>[]> = new Map();
|