@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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import {JavaScriptVisitor} from './visitor';
|
|
17
|
-
import {J, Type} from '../java';
|
|
17
|
+
import {J, Type, Expression, Statement, isIdentifier} from '../java';
|
|
18
18
|
import {JS, JSX} from './tree';
|
|
19
19
|
import {Cursor, Tree} from "../tree";
|
|
20
20
|
|
|
@@ -29,15 +29,27 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
29
29
|
*/
|
|
30
30
|
protected match: boolean = true;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Cursor tracking the current position in the target tree.
|
|
34
|
+
* Maintained in parallel with the pattern tree cursor (this.cursor).
|
|
35
|
+
*/
|
|
36
|
+
protected targetCursor?: Cursor;
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* Compares two AST trees.
|
|
34
|
-
*
|
|
35
|
-
* @param tree1 The first tree to compare
|
|
36
|
-
* @param tree2 The second tree to compare
|
|
40
|
+
*
|
|
41
|
+
* @param tree1 The first tree to compare (pattern tree)
|
|
42
|
+
* @param tree2 The second tree to compare (target tree)
|
|
43
|
+
* @param parentCursor1 Optional parent cursor for the pattern tree (for navigating to root)
|
|
44
|
+
* @param parentCursor2 Optional parent cursor for the target tree (for navigating to root)
|
|
37
45
|
* @returns true if the trees match, false otherwise
|
|
38
46
|
*/
|
|
39
|
-
async compare(tree1: J, tree2: J): Promise<boolean> {
|
|
47
|
+
async compare(tree1: J, tree2: J, parentCursor1?: Cursor, parentCursor2?: Cursor): Promise<boolean> {
|
|
40
48
|
this.match = true;
|
|
49
|
+
// Initialize targetCursor with parent if provided, otherwise undefined (will be set by visit())
|
|
50
|
+
this.targetCursor = parentCursor2;
|
|
51
|
+
// Initialize this.cursor (pattern cursor) with parent if provided
|
|
52
|
+
this.cursor = parentCursor1 || new Cursor(undefined, undefined);
|
|
41
53
|
await this.visit(tree1, tree2);
|
|
42
54
|
return this.match;
|
|
43
55
|
}
|
|
@@ -55,25 +67,158 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
69
|
* Aborts the visit operation by setting the match flag to false.
|
|
70
|
+
*
|
|
71
|
+
* @param t The node being compared
|
|
72
|
+
* @param reason Optional reason for the mismatch (e.g., 'kind-mismatch', 'property-mismatch')
|
|
73
|
+
* @param propertyName Optional property name where mismatch occurred
|
|
74
|
+
* @param expected Optional expected value
|
|
75
|
+
* @param actual Optional actual value
|
|
58
76
|
*/
|
|
59
|
-
protected abort<T>(t: T): T {
|
|
77
|
+
protected abort<T>(t: T, reason?: string, propertyName?: string, expected?: any, actual?: any): T {
|
|
60
78
|
this.match = false;
|
|
61
79
|
return t;
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Specialized abort methods for common mismatch scenarios.
|
|
84
|
+
* These provide a cleaner API at call sites.
|
|
85
|
+
* Can be overridden in subclasses to extract values from cursors and provide richer error messages.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
protected kindMismatch() {
|
|
89
|
+
const pattern = this.cursor?.value as any;
|
|
90
|
+
return this.abort(pattern, 'kind-mismatch');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected structuralMismatch(propertyName?: string) {
|
|
94
|
+
const pattern = this.cursor?.value as any;
|
|
95
|
+
return this.abort(pattern, 'structural-mismatch', propertyName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected arrayLengthMismatch(propertyName: string) {
|
|
99
|
+
const pattern = this.cursor?.value as any;
|
|
100
|
+
return this.abort(pattern, 'array-length-mismatch', propertyName);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected valueMismatch(propertyName?: string, expected?: any, actual?: any) {
|
|
104
|
+
const pattern = this.cursor?.value as any;
|
|
105
|
+
// If values not provided, try to extract from cursors (only if propertyName is available)
|
|
106
|
+
const expectedVal = expected !== undefined ? expected : (propertyName ? (pattern as any)?.[propertyName] : pattern);
|
|
107
|
+
const actualVal = actual !== undefined ? actual : (propertyName ? (this.targetCursor?.value as any)?.[propertyName] : this.targetCursor?.value);
|
|
108
|
+
return this.abort(pattern, 'value-mismatch', propertyName, expectedVal, actualVal);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected typeMismatch(propertyName?: string) {
|
|
112
|
+
const pattern = this.cursor?.value as any;
|
|
113
|
+
const target = this.targetCursor?.value as any;
|
|
114
|
+
return this.abort(pattern, 'type-mismatch', propertyName, pattern?.type, target?.type);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Helper method to visit an array property by iterating through both arrays in lock-step.
|
|
119
|
+
* Checks length mismatch first, then visits each element pair.
|
|
120
|
+
* Can be overridden in subclasses to add path tracking or other instrumentation.
|
|
121
|
+
*
|
|
122
|
+
* @param parent The parent node containing the array property
|
|
123
|
+
* @param propertyName The name of the array property
|
|
124
|
+
* @param array1 The array from the first tree
|
|
125
|
+
* @param array2 The array from the second tree
|
|
126
|
+
* @param visitor Function to visit each element pair (no need to return anything)
|
|
127
|
+
* @returns undefined, modifying this.match if a mismatch occurs
|
|
128
|
+
*/
|
|
129
|
+
protected async visitArrayProperty<T>(
|
|
130
|
+
parent: J,
|
|
131
|
+
propertyName: string,
|
|
132
|
+
array1: T[],
|
|
133
|
+
array2: T[],
|
|
134
|
+
visitor: (item1: T, item2: T, index: number) => Promise<void>
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
// Check length mismatch
|
|
137
|
+
if (array1.length !== array2.length) {
|
|
138
|
+
this.arrayLengthMismatch(propertyName);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Visit each element in lock step
|
|
143
|
+
for (let i = 0; i < array1.length; i++) {
|
|
144
|
+
await visitor(array1[i], array2[i], i);
|
|
145
|
+
if (!this.match) return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Helper method to visit a container property with proper context.
|
|
151
|
+
* Can be overridden in subclasses to add path tracking or other instrumentation.
|
|
152
|
+
*
|
|
153
|
+
* @param parent The parent node containing the container property
|
|
154
|
+
* @param propertyName The name of the container property
|
|
155
|
+
* @param container The container from the first tree
|
|
156
|
+
* @param otherContainer The container from the second tree
|
|
157
|
+
* @returns The container from the first tree
|
|
158
|
+
*/
|
|
159
|
+
protected async visitContainerProperty<T extends J>(
|
|
160
|
+
propertyName: string,
|
|
161
|
+
container: J.Container<T>,
|
|
162
|
+
otherContainer: J.Container<T>
|
|
163
|
+
): Promise<J.Container<T>> {
|
|
164
|
+
// Default implementation just calls visitContainer
|
|
165
|
+
// Subclasses can override to add property context
|
|
166
|
+
await this.visitContainer(container, otherContainer as any);
|
|
167
|
+
return container;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Helper to visit a RightPadded property with property context.
|
|
172
|
+
* This allows subclasses to track which property is being visited.
|
|
173
|
+
*
|
|
174
|
+
* @param propertyName The property name for context
|
|
175
|
+
* @param rightPadded The RightPadded from the first tree
|
|
176
|
+
* @param otherRightPadded The RightPadded from the second tree
|
|
177
|
+
* @returns The RightPadded from the first tree
|
|
178
|
+
*/
|
|
179
|
+
protected async visitRightPaddedProperty<T extends J | boolean>(
|
|
180
|
+
propertyName: string,
|
|
181
|
+
rightPadded: J.RightPadded<T>,
|
|
182
|
+
otherRightPadded: J.RightPadded<T>
|
|
183
|
+
): Promise<J.RightPadded<T>> {
|
|
184
|
+
// Default implementation just calls visitRightPadded
|
|
185
|
+
// Subclasses can override to add property context
|
|
186
|
+
return await this.visitRightPadded(rightPadded, otherRightPadded as any);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Helper to visit a LeftPadded property with property context.
|
|
191
|
+
* This allows subclasses to track which property is being visited.
|
|
192
|
+
*
|
|
193
|
+
* @param propertyName The property name for context
|
|
194
|
+
* @param leftPadded The LeftPadded from the first tree
|
|
195
|
+
* @param otherLeftPadded The LeftPadded from the second tree
|
|
196
|
+
* @returns The LeftPadded from the first tree
|
|
197
|
+
*/
|
|
198
|
+
protected async visitLeftPaddedProperty<T extends J | J.Space | number | string | boolean>(
|
|
199
|
+
propertyName: string,
|
|
200
|
+
leftPadded: J.LeftPadded<T>,
|
|
201
|
+
otherLeftPadded: J.LeftPadded<T>
|
|
202
|
+
): Promise<J.LeftPadded<T>> {
|
|
203
|
+
// Default implementation just calls visitLeftPadded
|
|
204
|
+
// Subclasses can override to add property context
|
|
205
|
+
return await this.visitLeftPadded(leftPadded, otherLeftPadded as any);
|
|
206
|
+
}
|
|
207
|
+
|
|
64
208
|
/**
|
|
65
209
|
* Generic method to visit a property value using the appropriate visitor method.
|
|
66
210
|
* This ensures wrappers (RightPadded, LeftPadded, Container) are properly tracked on the cursor.
|
|
67
211
|
*
|
|
68
212
|
* @param j The property value from the first tree
|
|
69
213
|
* @param other The corresponding property value from the second tree
|
|
214
|
+
* @param propertyName Optional property name for error reporting
|
|
70
215
|
* @returns The visited property value from the first tree
|
|
71
216
|
*/
|
|
72
|
-
protected async visitProperty(j: any, other: any): Promise<any> {
|
|
217
|
+
protected async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
|
|
73
218
|
// Handle null/undefined (but not other falsy values like 0, false, '')
|
|
74
219
|
if (j == null || other == null) {
|
|
75
220
|
if (j !== other) {
|
|
76
|
-
this.
|
|
221
|
+
return this.structuralMismatch(propertyName);
|
|
77
222
|
}
|
|
78
223
|
return j;
|
|
79
224
|
}
|
|
@@ -82,14 +227,20 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
82
227
|
|
|
83
228
|
// Check wrappers by kind
|
|
84
229
|
if (kind === J.Kind.RightPadded) {
|
|
85
|
-
return await this.
|
|
230
|
+
return propertyName ? await this.visitRightPaddedProperty(propertyName, j, other) :
|
|
231
|
+
await this.visitRightPadded(j, other);
|
|
86
232
|
}
|
|
87
233
|
|
|
88
234
|
if (kind === J.Kind.LeftPadded) {
|
|
89
|
-
return await this.
|
|
235
|
+
return propertyName ? await this.visitLeftPaddedProperty(propertyName, j, other) :
|
|
236
|
+
await this.visitLeftPadded(j, other);
|
|
90
237
|
}
|
|
91
238
|
|
|
92
239
|
if (kind === J.Kind.Container) {
|
|
240
|
+
// Use visitContainerProperty when propertyName is provided for proper context tracking
|
|
241
|
+
if (propertyName) {
|
|
242
|
+
return await this.visitContainerProperty(propertyName, j, other);
|
|
243
|
+
}
|
|
93
244
|
return await this.visitContainer(j, other);
|
|
94
245
|
}
|
|
95
246
|
|
|
@@ -109,7 +260,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
109
260
|
|
|
110
261
|
// For primitive values, compare directly
|
|
111
262
|
if (j !== other) {
|
|
112
|
-
this.
|
|
263
|
+
return this.valueMismatch(propertyName, j, other);
|
|
113
264
|
}
|
|
114
265
|
return j;
|
|
115
266
|
}
|
|
@@ -124,19 +275,17 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
124
275
|
* @returns The visited element from the first tree
|
|
125
276
|
*/
|
|
126
277
|
protected async visitElement<T extends J>(j: T, other: T): Promise<T> {
|
|
127
|
-
if (!this.match)
|
|
128
|
-
return j;
|
|
129
|
-
}
|
|
278
|
+
if (!this.match) return j;
|
|
130
279
|
|
|
131
280
|
// Check if kinds match
|
|
132
281
|
if (j.kind !== other.kind) {
|
|
133
|
-
return this.
|
|
282
|
+
return this.kindMismatch();
|
|
134
283
|
}
|
|
135
284
|
|
|
136
285
|
// Iterate over all properties
|
|
137
286
|
for (const key of Object.keys(j)) {
|
|
138
287
|
// Skip internal/private properties, id property, and markers property
|
|
139
|
-
if (key.startsWith('_') || key === 'id' || key === 'markers') {
|
|
288
|
+
if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers') {
|
|
140
289
|
continue;
|
|
141
290
|
}
|
|
142
291
|
|
|
@@ -146,22 +295,18 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
146
295
|
// Handle arrays - compare element by element
|
|
147
296
|
if (Array.isArray(jValue)) {
|
|
148
297
|
if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
|
|
149
|
-
return this.
|
|
298
|
+
return this.arrayLengthMismatch(key);
|
|
150
299
|
}
|
|
151
300
|
|
|
152
301
|
for (let i = 0; i < jValue.length; i++) {
|
|
153
|
-
await this.visitProperty(jValue[i], otherValue[i]);
|
|
154
|
-
if (!this.match)
|
|
155
|
-
return j;
|
|
156
|
-
}
|
|
302
|
+
await this.visitProperty(jValue[i], otherValue[i], `${key}[${i}]`);
|
|
303
|
+
if (!this.match) return j;
|
|
157
304
|
}
|
|
158
305
|
} else {
|
|
159
306
|
// Visit the property (which will handle wrappers, trees, primitives, etc.)
|
|
160
|
-
await this.visitProperty(jValue, otherValue);
|
|
307
|
+
await this.visitProperty(jValue, otherValue, key);
|
|
161
308
|
|
|
162
|
-
if (!this.match)
|
|
163
|
-
return j;
|
|
164
|
-
}
|
|
309
|
+
if (!this.match) return j;
|
|
165
310
|
}
|
|
166
311
|
}
|
|
167
312
|
|
|
@@ -170,41 +315,51 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
170
315
|
|
|
171
316
|
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
172
317
|
// If we've already found a mismatch, abort further processing
|
|
173
|
-
if (!this.match)
|
|
174
|
-
return j as R;
|
|
175
|
-
}
|
|
318
|
+
if (!this.match) return j as R;
|
|
176
319
|
|
|
177
320
|
// Check if the nodes have the same kind
|
|
178
321
|
if (!this.hasSameKind(j as J, p)) {
|
|
179
|
-
return this.
|
|
322
|
+
return this.kindMismatch() as R;
|
|
180
323
|
}
|
|
181
324
|
|
|
182
|
-
//
|
|
183
|
-
|
|
325
|
+
// Update targetCursor to track the target node in parallel with the pattern cursor
|
|
326
|
+
// (Can be overridden by subclasses if they need cursor access before calling super)
|
|
327
|
+
const savedTargetCursor = this.targetCursor;
|
|
328
|
+
this.targetCursor = new Cursor(p, this.targetCursor);
|
|
329
|
+
try {
|
|
330
|
+
// Continue with normal visitation, passing the other node as context
|
|
331
|
+
return await super.visit(j, p);
|
|
332
|
+
} finally {
|
|
333
|
+
this.targetCursor = savedTargetCursor;
|
|
334
|
+
}
|
|
184
335
|
}
|
|
185
336
|
|
|
186
337
|
/**
|
|
187
338
|
* Override visitRightPadded to compare only the elements, not markers or spacing.
|
|
188
339
|
* The context parameter p contains the corresponding element from the other tree.
|
|
189
340
|
* Pushes the wrapper onto the cursor stack so captures can access it.
|
|
341
|
+
* Also updates targetCursor in parallel.
|
|
190
342
|
*/
|
|
191
343
|
public async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
|
|
192
|
-
if (!this.match)
|
|
193
|
-
return right;
|
|
194
|
-
}
|
|
344
|
+
if (!this.match) return right;
|
|
195
345
|
|
|
196
346
|
// Extract the other element if it's also a RightPadded
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
347
|
+
const isRightPadded = (p as any).kind === J.Kind.RightPadded;
|
|
348
|
+
const otherWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
|
|
349
|
+
const otherElement = isRightPadded ? otherWrapper!.element : p;
|
|
200
350
|
|
|
201
|
-
// Push
|
|
351
|
+
// Push wrappers onto both cursors, then compare only the elements, not markers or spacing
|
|
202
352
|
const savedCursor = this.cursor;
|
|
353
|
+
const savedTargetCursor = this.targetCursor;
|
|
203
354
|
this.cursor = new Cursor(right, this.cursor);
|
|
355
|
+
this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
|
|
204
356
|
try {
|
|
357
|
+
// Call visitProperty without propertyName to avoid pushing spurious 'element' path entries
|
|
358
|
+
// The property context should be provided through visitRightPaddedProperty() if needed
|
|
205
359
|
await this.visitProperty(right.element, otherElement);
|
|
206
360
|
} finally {
|
|
207
361
|
this.cursor = savedCursor;
|
|
362
|
+
this.targetCursor = savedTargetCursor;
|
|
208
363
|
}
|
|
209
364
|
|
|
210
365
|
return right;
|
|
@@ -214,24 +369,28 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
214
369
|
* Override visitLeftPadded to compare only the elements, not markers or spacing.
|
|
215
370
|
* The context parameter p contains the corresponding element from the other tree.
|
|
216
371
|
* Pushes the wrapper onto the cursor stack so captures can access it.
|
|
372
|
+
* Also updates targetCursor in parallel.
|
|
217
373
|
*/
|
|
218
374
|
public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>, p: J): Promise<J.LeftPadded<T>> {
|
|
219
|
-
if (!this.match)
|
|
220
|
-
return left;
|
|
221
|
-
}
|
|
375
|
+
if (!this.match) return left;
|
|
222
376
|
|
|
223
377
|
// Extract the other element if it's also a LeftPadded
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
378
|
+
const isLeftPadded = (p as any).kind === J.Kind.LeftPadded;
|
|
379
|
+
const otherWrapper = isLeftPadded ? (p as unknown) as J.LeftPadded<T> : undefined;
|
|
380
|
+
const otherElement = isLeftPadded ? otherWrapper!.element : p;
|
|
227
381
|
|
|
228
|
-
// Push
|
|
382
|
+
// Push wrappers onto both cursors, then compare only the elements, not markers or spacing
|
|
229
383
|
const savedCursor = this.cursor;
|
|
384
|
+
const savedTargetCursor = this.targetCursor;
|
|
230
385
|
this.cursor = new Cursor(left, this.cursor);
|
|
386
|
+
this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
|
|
231
387
|
try {
|
|
388
|
+
// Call visitProperty without propertyName to avoid pushing spurious 'element' path entries
|
|
389
|
+
// The property context should be provided through visitLeftPaddedProperty() if needed
|
|
232
390
|
await this.visitProperty(left.element, otherElement);
|
|
233
391
|
} finally {
|
|
234
392
|
this.cursor = savedCursor;
|
|
393
|
+
this.targetCursor = savedTargetCursor;
|
|
235
394
|
}
|
|
236
395
|
|
|
237
396
|
return left;
|
|
@@ -241,34 +400,34 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
241
400
|
* Override visitContainer to compare only the elements, not markers or spacing.
|
|
242
401
|
* The context parameter p contains the corresponding element from the other tree.
|
|
243
402
|
* Pushes the wrapper onto the cursor stack so captures can access it.
|
|
403
|
+
* Also updates targetCursor in parallel.
|
|
244
404
|
*/
|
|
245
405
|
public async visitContainer<T extends J>(container: J.Container<T>, p: J): Promise<J.Container<T>> {
|
|
246
|
-
if (!this.match)
|
|
247
|
-
return container;
|
|
248
|
-
}
|
|
406
|
+
if (!this.match) return container;
|
|
249
407
|
|
|
250
408
|
// Extract the other elements if it's also a Container
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
409
|
+
const isContainer = (p as any).kind === J.Kind.Container;
|
|
410
|
+
const otherContainer = isContainer ? (p as unknown) as J.Container<T> : undefined;
|
|
411
|
+
const otherElements: J.RightPadded<T>[] = isContainer ? otherContainer!.elements : (p as any);
|
|
254
412
|
|
|
255
413
|
// Compare elements array length
|
|
256
414
|
if (container.elements.length !== otherElements.length) {
|
|
257
|
-
return this.
|
|
415
|
+
return this.arrayLengthMismatch('elements');
|
|
258
416
|
}
|
|
259
417
|
|
|
260
|
-
// Push
|
|
418
|
+
// Push wrappers onto both cursors, then compare each element
|
|
261
419
|
const savedCursor = this.cursor;
|
|
420
|
+
const savedTargetCursor = this.targetCursor;
|
|
262
421
|
this.cursor = new Cursor(container, this.cursor);
|
|
422
|
+
this.targetCursor = otherContainer ? new Cursor(otherContainer, this.targetCursor) : this.targetCursor;
|
|
263
423
|
try {
|
|
264
424
|
for (let i = 0; i < container.elements.length; i++) {
|
|
265
425
|
await this.visitProperty(container.elements[i], otherElements[i]);
|
|
266
|
-
if (!this.match)
|
|
267
|
-
return this.abort(container);
|
|
268
|
-
}
|
|
426
|
+
if (!this.match) return container;
|
|
269
427
|
}
|
|
270
428
|
} finally {
|
|
271
429
|
this.cursor = savedCursor;
|
|
430
|
+
this.targetCursor = savedTargetCursor;
|
|
272
431
|
}
|
|
273
432
|
|
|
274
433
|
return container;
|
|
@@ -1805,12 +1964,271 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
1805
1964
|
this.lenientTypeMatching = lenientTypeMatching;
|
|
1806
1965
|
}
|
|
1807
1966
|
|
|
1967
|
+
/**
|
|
1968
|
+
* Unwraps parentheses from a tree node recursively.
|
|
1969
|
+
* This allows comparing expressions with and without redundant parentheses.
|
|
1970
|
+
*
|
|
1971
|
+
* @param tree The tree to unwrap
|
|
1972
|
+
* @returns The unwrapped tree
|
|
1973
|
+
*/
|
|
1974
|
+
protected unwrap(tree: Tree | undefined): Tree | undefined {
|
|
1975
|
+
if (!tree) {
|
|
1976
|
+
return tree;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// Unwrap J.Parentheses nodes recursively
|
|
1980
|
+
if ((tree as any).kind === J.Kind.Parentheses) {
|
|
1981
|
+
const parens = tree as J.Parentheses<any>;
|
|
1982
|
+
return this.unwrap(parens.tree.element as Tree);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Unwrap J.ControlParentheses nodes recursively
|
|
1986
|
+
if ((tree as any).kind === J.Kind.ControlParentheses) {
|
|
1987
|
+
const controlParens = tree as J.ControlParentheses<any>;
|
|
1988
|
+
return this.unwrap(controlParens.tree.element as Tree);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
return tree;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
1995
|
+
// If we've already found a mismatch, abort further processing
|
|
1996
|
+
if (!this.match) return j as R;
|
|
1997
|
+
|
|
1998
|
+
// Unwrap parentheses from both trees before comparing
|
|
1999
|
+
const unwrappedJ = this.unwrap(j) || j;
|
|
2000
|
+
const unwrappedP = this.unwrap(p) || p;
|
|
2001
|
+
|
|
2002
|
+
// Skip the kind check that the base class does - semantic matching allows different kinds
|
|
2003
|
+
// (e.g., undefined identifier matching void expression)
|
|
2004
|
+
// Update targetCursor to track the target node in parallel with the pattern cursor
|
|
2005
|
+
const savedTargetCursor = this.targetCursor;
|
|
2006
|
+
this.targetCursor = new Cursor(unwrappedP, this.targetCursor);
|
|
2007
|
+
try {
|
|
2008
|
+
// Call the grandparent's visit to do actual visitation without the kind check
|
|
2009
|
+
return await JavaScriptVisitor.prototype.visit.call(this, unwrappedJ, unwrappedP) as R | undefined;
|
|
2010
|
+
} finally {
|
|
2011
|
+
this.targetCursor = savedTargetCursor;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* Override visitArrowFunction to allow semantic equivalence between expression body
|
|
2017
|
+
* and block with single return statement forms.
|
|
2018
|
+
*
|
|
2019
|
+
* Examples:
|
|
2020
|
+
* - `x => x + 1` matches `x => { return x + 1; }`
|
|
2021
|
+
* - `(x, y) => x + y` matches `(x, y) => { return x + y; }`
|
|
2022
|
+
*/
|
|
2023
|
+
override async visitArrowFunction(arrowFunction: JS.ArrowFunction, other: J): Promise<J | undefined> {
|
|
2024
|
+
if (!this.match) return arrowFunction;
|
|
2025
|
+
|
|
2026
|
+
if (other.kind !== JS.Kind.ArrowFunction) {
|
|
2027
|
+
return this.kindMismatch();
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
const otherArrow = other as JS.ArrowFunction;
|
|
2031
|
+
|
|
2032
|
+
// Compare all properties reflectively except lambda (handled specially below)
|
|
2033
|
+
for (const key of Object.keys(arrowFunction)) {
|
|
2034
|
+
if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'lambda') {
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
const jValue = (arrowFunction as any)[key];
|
|
2039
|
+
const otherValue = (otherArrow as any)[key];
|
|
2040
|
+
|
|
2041
|
+
// Handle arrays
|
|
2042
|
+
if (Array.isArray(jValue)) {
|
|
2043
|
+
if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
|
|
2044
|
+
return this.arrayLengthMismatch(key);
|
|
2045
|
+
}
|
|
2046
|
+
for (let i = 0; i < jValue.length; i++) {
|
|
2047
|
+
await this.visitProperty(jValue[i], otherValue[i]);
|
|
2048
|
+
if (!this.match) return arrowFunction;
|
|
2049
|
+
}
|
|
2050
|
+
} else {
|
|
2051
|
+
await this.visitProperty(jValue, otherValue);
|
|
2052
|
+
if (!this.match) return arrowFunction;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Compare lambda parameters
|
|
2057
|
+
const params1 = arrowFunction.lambda.parameters.parameters;
|
|
2058
|
+
const params2 = otherArrow.lambda.parameters.parameters;
|
|
2059
|
+
if (params1.length !== params2.length) {
|
|
2060
|
+
return this.arrayLengthMismatch('lambda.parameters.parameters');
|
|
2061
|
+
}
|
|
2062
|
+
for (let i = 0; i < params1.length; i++) {
|
|
2063
|
+
await this.visitProperty(params1[i], params2[i]);
|
|
2064
|
+
if (!this.match) return arrowFunction;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// Handle semantic equivalence for lambda bodies
|
|
2068
|
+
const body1 = arrowFunction.lambda.body;
|
|
2069
|
+
const body2 = otherArrow.lambda.body;
|
|
2070
|
+
|
|
2071
|
+
// Try to extract the expression from each body
|
|
2072
|
+
const expr1 = this.extractExpression(body1);
|
|
2073
|
+
const expr2 = this.extractExpression(body2);
|
|
2074
|
+
|
|
2075
|
+
if (expr1 && expr2) {
|
|
2076
|
+
// Both have extractable expressions - compare them
|
|
2077
|
+
await this.visit(expr1, expr2);
|
|
2078
|
+
} else {
|
|
2079
|
+
// At least one is not a simple expression or block-with-return
|
|
2080
|
+
// Fall back to exact comparison
|
|
2081
|
+
await this.visit(body1, body2);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
return arrowFunction;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Override visitLambdaParameters to allow semantic equivalence between
|
|
2089
|
+
* arrow functions with and without parentheses around single parameters.
|
|
2090
|
+
*
|
|
2091
|
+
* Examples:
|
|
2092
|
+
* - `x => x + 1` matches `(x) => x + 1`
|
|
2093
|
+
*/
|
|
2094
|
+
override async visitLambdaParameters(parameters: J.Lambda.Parameters, other: J): Promise<J | undefined> {
|
|
2095
|
+
if (!this.match) return parameters;
|
|
2096
|
+
|
|
2097
|
+
if (other.kind !== J.Kind.LambdaParameters) {
|
|
2098
|
+
return this.kindMismatch();
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const otherParams = other as J.Lambda.Parameters;
|
|
2102
|
+
|
|
2103
|
+
// Compare all properties except 'parenthesized' using reflection
|
|
2104
|
+
for (const key of Object.keys(parameters)) {
|
|
2105
|
+
if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'parenthesized') {
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const jValue = (parameters as any)[key];
|
|
2110
|
+
const otherValue = (otherParams as any)[key];
|
|
2111
|
+
|
|
2112
|
+
// Handle arrays
|
|
2113
|
+
if (Array.isArray(jValue)) {
|
|
2114
|
+
if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
|
|
2115
|
+
return this.arrayLengthMismatch(key);
|
|
2116
|
+
}
|
|
2117
|
+
for (let i = 0; i < jValue.length; i++) {
|
|
2118
|
+
await this.visitProperty(jValue[i], otherValue[i]);
|
|
2119
|
+
if (!this.match) return parameters;
|
|
2120
|
+
}
|
|
2121
|
+
} else {
|
|
2122
|
+
await this.visitProperty(jValue, otherValue);
|
|
2123
|
+
if (!this.match) return parameters;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
return parameters;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
/**
|
|
2131
|
+
* Override visitPropertyAssignment to allow semantic equivalence between
|
|
2132
|
+
* object property shorthand and longhand forms.
|
|
2133
|
+
*
|
|
2134
|
+
* Examples:
|
|
2135
|
+
* - `{ x }` matches `{ x: x }`
|
|
2136
|
+
* - `{ x: x, y: y }` matches `{ x, y }`
|
|
2137
|
+
*/
|
|
2138
|
+
override async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, other: J): Promise<J | undefined> {
|
|
2139
|
+
if (!this.match) return propertyAssignment;
|
|
2140
|
+
|
|
2141
|
+
if (other.kind !== JS.Kind.PropertyAssignment) {
|
|
2142
|
+
return this.kindMismatch();
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
const otherProp = other as JS.PropertyAssignment;
|
|
2146
|
+
|
|
2147
|
+
// Extract property names for semantic comparison
|
|
2148
|
+
const propName = this.getPropertyName(propertyAssignment);
|
|
2149
|
+
const otherPropName = this.getPropertyName(otherProp);
|
|
2150
|
+
|
|
2151
|
+
// Names must match
|
|
2152
|
+
if (!propName || !otherPropName || propName !== otherPropName) {
|
|
2153
|
+
// Can't do semantic comparison without identifiers, fall back to exact comparison
|
|
2154
|
+
return await super.visitPropertyAssignment(propertyAssignment, other);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// Detect shorthand (no initializer) vs longhand (has initializer)
|
|
2158
|
+
const isShorthand1 = !propertyAssignment.initializer;
|
|
2159
|
+
const isShorthand2 = !otherProp.initializer;
|
|
2160
|
+
|
|
2161
|
+
if (isShorthand1 === isShorthand2) {
|
|
2162
|
+
// Both shorthand or both longhand - use base comparison
|
|
2163
|
+
return await super.visitPropertyAssignment(propertyAssignment, other);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// One is shorthand, one is longhand - check semantic equivalence
|
|
2167
|
+
const longhandProp = isShorthand1 ? otherProp : propertyAssignment;
|
|
2168
|
+
|
|
2169
|
+
// Check if the longhand's initializer is an identifier with the same name as the property
|
|
2170
|
+
if (this.isIdentifierWithName(longhandProp.initializer, propName)) {
|
|
2171
|
+
// Semantically equivalent!
|
|
2172
|
+
return propertyAssignment;
|
|
2173
|
+
} else {
|
|
2174
|
+
// Not equivalent (e.g., { x: y })
|
|
2175
|
+
return this.structuralMismatch('initializer');
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
/**
|
|
2180
|
+
* Extracts the property name from a PropertyAssignment.
|
|
2181
|
+
* Returns the simple name if the property is an identifier, undefined otherwise.
|
|
2182
|
+
*/
|
|
2183
|
+
private getPropertyName(prop: JS.PropertyAssignment): string | undefined {
|
|
2184
|
+
const nameExpr = prop.name.element;
|
|
2185
|
+
return isIdentifier(nameExpr) ? nameExpr.simpleName : undefined;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/**
|
|
2189
|
+
* Checks if an expression is an identifier with the given name.
|
|
2190
|
+
*/
|
|
2191
|
+
private isIdentifierWithName(expr: Expression | undefined, name: string): boolean | undefined {
|
|
2192
|
+
return expr && isIdentifier(expr) && expr.simpleName === name;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Extracts the expression from an arrow function body.
|
|
2197
|
+
* Returns the expression if:
|
|
2198
|
+
* - body is already an Expression, OR
|
|
2199
|
+
* - body is a Block with exactly one Return statement
|
|
2200
|
+
* Otherwise returns undefined.
|
|
2201
|
+
*/
|
|
2202
|
+
private extractExpression(body: Statement | Expression): Expression | undefined {
|
|
2203
|
+
// If it's already an expression, return it
|
|
2204
|
+
if ((body as any).kind !== J.Kind.Block) {
|
|
2205
|
+
return body as Expression;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// It's a block - check if it contains exactly one return statement
|
|
2209
|
+
const block = body as J.Block;
|
|
2210
|
+
if (block.statements.length !== 1) {
|
|
2211
|
+
return undefined;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Unwrap the RightPadded wrapper from the statement
|
|
2215
|
+
const stmtWrapper = block.statements[0];
|
|
2216
|
+
const stmt = stmtWrapper.element;
|
|
2217
|
+
|
|
2218
|
+
if ((stmt as any).kind !== J.Kind.Return) {
|
|
2219
|
+
return undefined;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
const returnStmt = stmt as J.Return;
|
|
2223
|
+
return returnStmt.expression;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1808
2226
|
/**
|
|
1809
2227
|
* Override visitProperty to allow lenient type matching.
|
|
1810
2228
|
* When lenientTypeMatching is enabled, null vs Type comparisons are allowed
|
|
1811
2229
|
* (where one value is null/undefined and the other is a Type object).
|
|
1812
2230
|
*/
|
|
1813
|
-
protected override async visitProperty(j: any, other: any): Promise<any> {
|
|
2231
|
+
protected override async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
|
|
1814
2232
|
// Handle null/undefined with lenient type matching
|
|
1815
2233
|
if (this.lenientTypeMatching && (j == null || other == null)) {
|
|
1816
2234
|
if (j !== other) {
|
|
@@ -1822,7 +2240,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
1822
2240
|
(otherKind && typeof otherKind === 'string' && otherKind.startsWith('org.openrewrite.java.tree.JavaType$'));
|
|
1823
2241
|
|
|
1824
2242
|
if (!isTypeComparison) {
|
|
1825
|
-
this.
|
|
2243
|
+
this.structuralMismatch(propertyName!);
|
|
1826
2244
|
}
|
|
1827
2245
|
}
|
|
1828
2246
|
return j;
|
|
@@ -1843,8 +2261,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
1843
2261
|
return this.lenientTypeMatching ? true : target === source;
|
|
1844
2262
|
}
|
|
1845
2263
|
|
|
1846
|
-
if (target.kind !== source.kind) {
|
|
1847
|
-
|
|
2264
|
+
if (target.kind !== source.kind && (target.kind == Type.Kind.Unknown || source.kind == Type.Kind.Unknown)) {
|
|
2265
|
+
// In lenient mode, allow kind mismatches (e.g., Unknown vs proper type)
|
|
2266
|
+
// This handles cases where pattern has unresolved types
|
|
2267
|
+
return this.lenientTypeMatching;
|
|
1848
2268
|
}
|
|
1849
2269
|
|
|
1850
2270
|
// For method types, check declaring type
|
|
@@ -1917,119 +2337,150 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
1917
2337
|
*/
|
|
1918
2338
|
override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
|
|
1919
2339
|
if (other.kind !== J.Kind.MethodInvocation) {
|
|
1920
|
-
return this.
|
|
2340
|
+
return this.kindMismatch();
|
|
1921
2341
|
}
|
|
1922
2342
|
|
|
1923
2343
|
const otherMethod = other as J.MethodInvocation;
|
|
1924
2344
|
|
|
1925
|
-
// Check
|
|
1926
|
-
if
|
|
1927
|
-
|
|
1928
|
-
|
|
2345
|
+
// Check if we can skip name checking based on type attribution
|
|
2346
|
+
// We can only skip the name check if both have method types AND they represent the SAME method
|
|
2347
|
+
// (not just type-compatible methods, but the actual same function with same FQN)
|
|
2348
|
+
let canSkipNameCheck = false;
|
|
2349
|
+
if (method.methodType && otherMethod.methodType) {
|
|
2350
|
+
// Check if both method types have fully qualified declaring types with the same FQN
|
|
2351
|
+
// This indicates they're the same method from the same module (possibly aliased)
|
|
2352
|
+
const methodDeclaringType = method.methodType.declaringType;
|
|
2353
|
+
const otherDeclaringType = otherMethod.methodType.declaringType;
|
|
2354
|
+
|
|
2355
|
+
if (methodDeclaringType && otherDeclaringType &&
|
|
2356
|
+
Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType)) {
|
|
2357
|
+
|
|
2358
|
+
const methodFQN = Type.FullyQualified.getFullyQualifiedName(methodDeclaringType as Type.FullyQualified);
|
|
2359
|
+
const otherFQN = Type.FullyQualified.getFullyQualifiedName(otherDeclaringType as Type.FullyQualified);
|
|
2360
|
+
|
|
2361
|
+
// Same module/class AND same method name in the type = same method (can be aliased)
|
|
2362
|
+
if (methodFQN === otherFQN && method.methodType.name === otherMethod.methodType.name) {
|
|
2363
|
+
canSkipNameCheck = true;
|
|
2364
|
+
}
|
|
2365
|
+
// If FQNs or method names don't match, we can't skip name check - fall through to name checking
|
|
2366
|
+
}
|
|
2367
|
+
// If one or both don't have fully qualified types, we can't safely skip name checking
|
|
2368
|
+
// Fall through to normal name comparison below
|
|
1929
2369
|
}
|
|
1930
2370
|
|
|
1931
|
-
// Check type
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
if (this.lenientTypeMatching) {
|
|
1936
|
-
return super.visitMethodInvocation(method, other);
|
|
2371
|
+
// Check names unless we determined we can skip based on type FQN matching
|
|
2372
|
+
if (!canSkipNameCheck) {
|
|
2373
|
+
if (method.name.simpleName !== otherMethod.name.simpleName) {
|
|
2374
|
+
return this.valueMismatch('name.simpleName', method.name.simpleName, otherMethod.name.simpleName);
|
|
1937
2375
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2376
|
+
|
|
2377
|
+
// In strict mode, check type attribution requirements
|
|
2378
|
+
if (!this.lenientTypeMatching) {
|
|
2379
|
+
// Strict mode: if one has type but the other doesn't, they don't match
|
|
2380
|
+
if ((method.methodType && !otherMethod.methodType) ||
|
|
2381
|
+
(!method.methodType && otherMethod.methodType)) {
|
|
2382
|
+
return this.typeMismatch('methodType');
|
|
2383
|
+
}
|
|
1941
2384
|
}
|
|
1942
|
-
// If neither has type, fall through to structural comparison
|
|
1943
|
-
return super.visitMethodInvocation(method, other);
|
|
1944
|
-
}
|
|
1945
2385
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
return this.abort(method);
|
|
1951
|
-
}
|
|
2386
|
+
// If neither has type, use structural comparison
|
|
2387
|
+
if (!method.methodType && !otherMethod.methodType) {
|
|
2388
|
+
return super.visitMethodInvocation(method, other);
|
|
2389
|
+
}
|
|
1952
2390
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2391
|
+
// If both have types with FQ declaring types, verify they're compatible
|
|
2392
|
+
// (This prevents matching completely different methods like util.isArray vs util.isBoolean)
|
|
2393
|
+
if (method.methodType && otherMethod.methodType) {
|
|
2394
|
+
const methodDeclaringType = method.methodType.declaringType;
|
|
2395
|
+
const otherDeclaringType = otherMethod.methodType.declaringType;
|
|
1957
2396
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
(!method.select && otherMethod.select) ||
|
|
1961
|
-
(method.select && !otherMethod.select);
|
|
2397
|
+
if (methodDeclaringType && otherDeclaringType &&
|
|
2398
|
+
Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType)) {
|
|
1962
2399
|
|
|
1963
|
-
|
|
1964
|
-
|
|
2400
|
+
const methodFQN = Type.FullyQualified.getFullyQualifiedName(methodDeclaringType as Type.FullyQualified);
|
|
2401
|
+
const otherFQN = Type.FullyQualified.getFullyQualifiedName(otherDeclaringType as Type.FullyQualified);
|
|
2402
|
+
|
|
2403
|
+
// Different declaring types = different methods, even with same name
|
|
2404
|
+
if (methodFQN !== otherFQN) {
|
|
2405
|
+
return this.valueMismatch('methodType.declaringType');
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// When types match (canSkipNameCheck = true), we can skip select comparison entirely.
|
|
2412
|
+
// This allows matching forwardRef() vs React.forwardRef() where types indicate same method.
|
|
2413
|
+
if (!canSkipNameCheck) {
|
|
2414
|
+
// Types didn't provide a match - must compare receivers structurally
|
|
1965
2415
|
if ((method.select === undefined) !== (otherMethod.select === undefined)) {
|
|
1966
|
-
return this.
|
|
2416
|
+
return this.structuralMismatch('select');
|
|
1967
2417
|
}
|
|
1968
2418
|
|
|
1969
2419
|
if (method.select && otherMethod.select) {
|
|
1970
|
-
await this.
|
|
2420
|
+
await this.visitRightPaddedProperty('select', method.select, otherMethod.select as any);
|
|
2421
|
+
if (!this.match) return method;
|
|
1971
2422
|
}
|
|
1972
2423
|
}
|
|
2424
|
+
// else: types matched, skip select comparison (allows namespace vs named imports)
|
|
1973
2425
|
|
|
1974
2426
|
// Compare type parameters
|
|
1975
2427
|
if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
|
|
1976
|
-
return this.
|
|
2428
|
+
return this.structuralMismatch('typeParameters');
|
|
1977
2429
|
}
|
|
1978
2430
|
|
|
1979
2431
|
if (method.typeParameters && otherMethod.typeParameters) {
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
}
|
|
1983
|
-
for (let i = 0; i < method.typeParameters.elements.length; i++) {
|
|
1984
|
-
await this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
|
|
1985
|
-
if (!this.match) {
|
|
1986
|
-
return this.abort(method);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
2432
|
+
await this.visitContainerProperty('typeParameters', method.typeParameters, otherMethod.typeParameters);
|
|
2433
|
+
if (!this.match) return method;
|
|
1989
2434
|
}
|
|
1990
2435
|
|
|
1991
|
-
// Compare name
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2436
|
+
// Compare name
|
|
2437
|
+
// If we determined we can skip name check (same FQN method, possibly aliased), skip it
|
|
2438
|
+
// This allows matching aliased imports where names differ but types are the same
|
|
2439
|
+
if (!canSkipNameCheck) {
|
|
2440
|
+
await this.visit(method.name, otherMethod.name);
|
|
2441
|
+
if (!this.match) return method;
|
|
1995
2442
|
}
|
|
1996
2443
|
|
|
1997
2444
|
// Compare arguments
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (!this.match) {
|
|
2001
|
-
return this.abort(method);
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2445
|
+
await this.visitContainerProperty('arguments', method.arguments, otherMethod.arguments);
|
|
2446
|
+
if (!this.match) return method;
|
|
2004
2447
|
|
|
2005
2448
|
return method;
|
|
2006
2449
|
}
|
|
2007
2450
|
|
|
2008
2451
|
/**
|
|
2009
|
-
* Override identifier comparison to include
|
|
2452
|
+
* Override identifier comparison to include:
|
|
2453
|
+
* 1. Type checking for field access
|
|
2454
|
+
* 2. Semantic equivalence between `undefined` identifier and void expressions
|
|
2010
2455
|
*/
|
|
2011
2456
|
override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
|
|
2457
|
+
// Check if this identifier is "undefined" and the other is a void expression
|
|
2458
|
+
if (identifier.simpleName === 'undefined' && (other as any).kind === JS.Kind.Void) {
|
|
2459
|
+
// Both evaluate to undefined, so they match
|
|
2460
|
+
return identifier;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2012
2463
|
if (other.kind !== J.Kind.Identifier) {
|
|
2013
|
-
return this.
|
|
2464
|
+
return this.kindMismatch();
|
|
2014
2465
|
}
|
|
2015
2466
|
|
|
2016
2467
|
const otherIdentifier = other as J.Identifier;
|
|
2017
2468
|
|
|
2018
2469
|
// Check name matches
|
|
2019
2470
|
if (identifier.simpleName !== otherIdentifier.simpleName) {
|
|
2020
|
-
return this.
|
|
2471
|
+
return this.valueMismatch('simpleName');
|
|
2021
2472
|
}
|
|
2022
2473
|
|
|
2023
2474
|
// For identifiers with field types, check type attribution
|
|
2024
2475
|
if (identifier.fieldType && otherIdentifier.fieldType) {
|
|
2025
2476
|
if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
|
|
2026
|
-
return this.
|
|
2477
|
+
return this.typeMismatch('fieldType');
|
|
2027
2478
|
}
|
|
2028
2479
|
} else if (identifier.fieldType || otherIdentifier.fieldType) {
|
|
2029
2480
|
// Lenient mode: if either has no type, allow structural matching
|
|
2030
2481
|
if (!this.lenientTypeMatching) {
|
|
2031
2482
|
// Strict mode: if only one has a type, they don't match
|
|
2032
|
-
return this.
|
|
2483
|
+
return this.typeMismatch('fieldType');
|
|
2033
2484
|
}
|
|
2034
2485
|
}
|
|
2035
2486
|
|
|
@@ -2045,29 +2496,29 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
2045
2496
|
const otherVariableDeclarations = other as J.VariableDeclarations;
|
|
2046
2497
|
|
|
2047
2498
|
// Visit leading annotations
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
await this.visit(
|
|
2054
|
-
|
|
2055
|
-
|
|
2499
|
+
await this.visitArrayProperty(
|
|
2500
|
+
variableDeclarations,
|
|
2501
|
+
'leadingAnnotations',
|
|
2502
|
+
variableDeclarations.leadingAnnotations,
|
|
2503
|
+
otherVariableDeclarations.leadingAnnotations,
|
|
2504
|
+
async (ann1, ann2) => { await this.visit(ann1, ann2); }
|
|
2505
|
+
);
|
|
2506
|
+
if (!this.match) return variableDeclarations;
|
|
2056
2507
|
|
|
2057
2508
|
// Visit modifiers
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
await this.visit(
|
|
2064
|
-
|
|
2065
|
-
|
|
2509
|
+
await this.visitArrayProperty(
|
|
2510
|
+
variableDeclarations,
|
|
2511
|
+
'modifiers',
|
|
2512
|
+
variableDeclarations.modifiers,
|
|
2513
|
+
otherVariableDeclarations.modifiers,
|
|
2514
|
+
async (mod1, mod2) => { await this.visit(mod1, mod2); }
|
|
2515
|
+
);
|
|
2516
|
+
if (!this.match) return variableDeclarations;
|
|
2066
2517
|
|
|
2067
2518
|
// Compare typeExpression - lenient matching allows one to be undefined
|
|
2068
2519
|
if ((variableDeclarations.typeExpression === undefined) !== (otherVariableDeclarations.typeExpression === undefined)) {
|
|
2069
2520
|
if (!this.lenientTypeMatching) {
|
|
2070
|
-
return this.
|
|
2521
|
+
return this.structuralMismatch('typeExpression');
|
|
2071
2522
|
}
|
|
2072
2523
|
// In lenient mode, skip type comparison and continue
|
|
2073
2524
|
} else if (variableDeclarations.typeExpression && otherVariableDeclarations.typeExpression) {
|
|
@@ -2078,19 +2529,18 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
2078
2529
|
|
|
2079
2530
|
// Compare varargs
|
|
2080
2531
|
if ((variableDeclarations.varargs === undefined) !== (otherVariableDeclarations.varargs === undefined)) {
|
|
2081
|
-
return this.
|
|
2532
|
+
return this.structuralMismatch('varargs');
|
|
2082
2533
|
}
|
|
2083
2534
|
|
|
2084
2535
|
// Compare variables
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
}
|
|
2536
|
+
await this.visitArrayProperty(
|
|
2537
|
+
variableDeclarations,
|
|
2538
|
+
'variables',
|
|
2539
|
+
variableDeclarations.variables,
|
|
2540
|
+
otherVariableDeclarations.variables,
|
|
2541
|
+
async (var1, var2) => { await this.visitRightPadded(var1, var2 as any); }
|
|
2542
|
+
);
|
|
2543
|
+
if (!this.match) return variableDeclarations;
|
|
2094
2544
|
|
|
2095
2545
|
return variableDeclarations;
|
|
2096
2546
|
}
|
|
@@ -2104,28 +2554,28 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
2104
2554
|
const otherMethodDeclaration = other as J.MethodDeclaration;
|
|
2105
2555
|
|
|
2106
2556
|
// Visit leading annotations
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
await this.visit(
|
|
2113
|
-
|
|
2114
|
-
|
|
2557
|
+
await this.visitArrayProperty(
|
|
2558
|
+
methodDeclaration,
|
|
2559
|
+
'leadingAnnotations',
|
|
2560
|
+
methodDeclaration.leadingAnnotations,
|
|
2561
|
+
otherMethodDeclaration.leadingAnnotations,
|
|
2562
|
+
async (ann1, ann2) => { await this.visit(ann1, ann2); }
|
|
2563
|
+
);
|
|
2564
|
+
if (!this.match) return methodDeclaration;
|
|
2115
2565
|
|
|
2116
2566
|
// Visit modifiers
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
await this.visit(
|
|
2123
|
-
|
|
2124
|
-
|
|
2567
|
+
await this.visitArrayProperty(
|
|
2568
|
+
methodDeclaration,
|
|
2569
|
+
'modifiers',
|
|
2570
|
+
methodDeclaration.modifiers,
|
|
2571
|
+
otherMethodDeclaration.modifiers,
|
|
2572
|
+
async (mod1, mod2) => { await this.visit(mod1, mod2); }
|
|
2573
|
+
);
|
|
2574
|
+
if (!this.match) return methodDeclaration;
|
|
2125
2575
|
|
|
2126
2576
|
// Visit type parameters if present
|
|
2127
2577
|
if (!!methodDeclaration.typeParameters !== !!otherMethodDeclaration.typeParameters) {
|
|
2128
|
-
return this.
|
|
2578
|
+
return this.structuralMismatch('typeParameters');
|
|
2129
2579
|
}
|
|
2130
2580
|
|
|
2131
2581
|
if (methodDeclaration.typeParameters && otherMethodDeclaration.typeParameters) {
|
|
@@ -2136,7 +2586,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
2136
2586
|
// Compare returnTypeExpression - lenient matching allows one to be undefined
|
|
2137
2587
|
if ((methodDeclaration.returnTypeExpression === undefined) !== (otherMethodDeclaration.returnTypeExpression === undefined)) {
|
|
2138
2588
|
if (!this.lenientTypeMatching) {
|
|
2139
|
-
return this.
|
|
2589
|
+
return this.typeMismatch('returnTypeExpression');
|
|
2140
2590
|
}
|
|
2141
2591
|
// In lenient mode, skip type comparison and continue
|
|
2142
2592
|
} else if (methodDeclaration.returnTypeExpression && otherMethodDeclaration.returnTypeExpression) {
|
|
@@ -2150,36 +2600,22 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
2150
2600
|
if (!this.match) return methodDeclaration;
|
|
2151
2601
|
|
|
2152
2602
|
// Compare parameters
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
// Visit each parameter in lock step
|
|
2158
|
-
for (let i = 0; i < methodDeclaration.parameters.elements.length; i++) {
|
|
2159
|
-
await this.visit(methodDeclaration.parameters.elements[i].element, otherMethodDeclaration.parameters.elements[i].element);
|
|
2160
|
-
if (!this.match) return methodDeclaration;
|
|
2161
|
-
}
|
|
2603
|
+
await this.visitContainer(methodDeclaration.parameters, otherMethodDeclaration.parameters as any);
|
|
2604
|
+
if (!this.match) return methodDeclaration;
|
|
2162
2605
|
|
|
2163
2606
|
// Visit throws if present
|
|
2164
2607
|
if (!!methodDeclaration.throws !== !!otherMethodDeclaration.throws) {
|
|
2165
|
-
return this.
|
|
2608
|
+
return this.structuralMismatch('throws');
|
|
2166
2609
|
}
|
|
2167
2610
|
|
|
2168
2611
|
if (methodDeclaration.throws && otherMethodDeclaration.throws) {
|
|
2169
|
-
|
|
2170
|
-
if (
|
|
2171
|
-
return this.abort(methodDeclaration);
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
for (let i = 0; i < methodDeclaration.throws.elements.length; i++) {
|
|
2175
|
-
await this.visit(methodDeclaration.throws.elements[i].element, otherMethodDeclaration.throws.elements[i].element);
|
|
2176
|
-
if (!this.match) return methodDeclaration;
|
|
2177
|
-
}
|
|
2612
|
+
await this.visitContainer(methodDeclaration.throws, otherMethodDeclaration.throws as any);
|
|
2613
|
+
if (!this.match) return methodDeclaration;
|
|
2178
2614
|
}
|
|
2179
2615
|
|
|
2180
2616
|
// Visit body if present
|
|
2181
2617
|
if (!!methodDeclaration.body !== !!otherMethodDeclaration.body) {
|
|
2182
|
-
return this.
|
|
2618
|
+
return this.structuralMismatch('body');
|
|
2183
2619
|
}
|
|
2184
2620
|
|
|
2185
2621
|
if (methodDeclaration.body && otherMethodDeclaration.body) {
|
|
@@ -2189,4 +2625,59 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
|
|
|
2189
2625
|
|
|
2190
2626
|
return methodDeclaration;
|
|
2191
2627
|
}
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* Override visitVoid to allow semantic equivalence with undefined identifier.
|
|
2631
|
+
* This handles the reverse case where the pattern is a void expression
|
|
2632
|
+
* and the source is the undefined identifier.
|
|
2633
|
+
*
|
|
2634
|
+
* Examples:
|
|
2635
|
+
* - `void 0` matches `undefined`
|
|
2636
|
+
* - `void(0)` matches `undefined`
|
|
2637
|
+
* - `void 1` matches `undefined`
|
|
2638
|
+
*/
|
|
2639
|
+
override async visitVoid(voidExpr: JS.Void, other: J): Promise<J | undefined> {
|
|
2640
|
+
if (!this.match) return voidExpr;
|
|
2641
|
+
|
|
2642
|
+
// Check if the other is an undefined identifier
|
|
2643
|
+
if ((other as any).kind === J.Kind.Identifier) {
|
|
2644
|
+
const identifier = other as J.Identifier;
|
|
2645
|
+
if (identifier.simpleName === 'undefined') {
|
|
2646
|
+
// Both evaluate to undefined, so they match
|
|
2647
|
+
return voidExpr;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// Otherwise delegate to parent
|
|
2652
|
+
return super.visitVoid(voidExpr, other as any);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* Override visitLiteral to allow semantic equivalence between
|
|
2657
|
+
* different numeric literal formats.
|
|
2658
|
+
*
|
|
2659
|
+
* Examples:
|
|
2660
|
+
* - `255` matches `0xFF`
|
|
2661
|
+
* - `255` matches `0o377`
|
|
2662
|
+
* - `255` matches `0b11111111`
|
|
2663
|
+
* - `1000` matches `1e3`
|
|
2664
|
+
*/
|
|
2665
|
+
override async visitLiteral(literal: J.Literal, other: J): Promise<J | undefined> {
|
|
2666
|
+
if (!this.match) return literal;
|
|
2667
|
+
|
|
2668
|
+
if ((other as any).kind !== J.Kind.Literal) {
|
|
2669
|
+
return await super.visitLiteral(literal, other);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
const otherLiteral = other as J.Literal;
|
|
2673
|
+
|
|
2674
|
+
// Only compare value and type, ignoring valueSource (text representation) and unicodeEscapes
|
|
2675
|
+
await this.visitProperty(literal.value, otherLiteral.value, 'value');
|
|
2676
|
+
if (!this.match) return literal;
|
|
2677
|
+
|
|
2678
|
+
await this.visitProperty(literal.type, otherLiteral.type, 'type');
|
|
2679
|
+
if (!this.match) return literal;
|
|
2680
|
+
|
|
2681
|
+
return literal;
|
|
2682
|
+
}
|
|
2192
2683
|
}
|