@openrewrite/rewrite 8.91.0 → 8.91.1
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/formatting-utils.d.ts +8 -5
- package/dist/java/formatting-utils.d.ts.map +1 -1
- package/dist/java/formatting-utils.js +14 -10
- package/dist/java/formatting-utils.js.map +1 -1
- package/dist/java/type.d.ts +2 -0
- package/dist/java/type.d.ts.map +1 -1
- package/dist/java/type.js +5 -0
- package/dist/java/type.js.map +1 -1
- package/dist/javascript/add-import.d.ts +39 -20
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +513 -243
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/assertions.d.ts +7 -0
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +31 -5
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts +5 -0
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +34 -9
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +8 -9
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
- package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts +19 -1
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +37 -2
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/package-exported-types.d.ts.map +1 -1
- package/dist/javascript/package-exported-types.js +56 -72
- package/dist/javascript/package-exported-types.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +10 -3
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/change-import.d.ts.map +1 -1
- package/dist/javascript/recipes/change-import.js +4 -8
- package/dist/javascript/recipes/change-import.js.map +1 -1
- package/dist/javascript/style.d.ts.map +1 -1
- package/dist/javascript/style.js +3 -10
- package/dist/javascript/style.js.map +1 -1
- package/dist/javascript/templating/bindings.d.ts +19 -0
- package/dist/javascript/templating/bindings.d.ts.map +1 -0
- package/dist/javascript/templating/bindings.js +121 -0
- package/dist/javascript/templating/bindings.js.map +1 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +5 -6
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +18 -7
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +70 -42
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/index.d.ts +1 -1
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +13 -10
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +14 -5
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +14 -18
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +8 -0
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +43 -3
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +47 -1
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +5 -0
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +17 -0
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts +1 -0
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +52 -2
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/style.d.ts +2 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +5 -0
- package/dist/style.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +12 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +40 -3
- package/dist/test/rewrite-test.js.map +1 -1
- package/package.json +1 -1
- package/src/java/formatting-utils.ts +14 -10
- package/src/java/type.ts +5 -0
- package/src/javascript/add-import.ts +590 -230
- package/src/javascript/assertions.ts +33 -5
- package/src/javascript/comparator.ts +39 -10
- package/src/javascript/format/format.ts +9 -10
- package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
- package/src/javascript/node-resolution-result.ts +59 -4
- package/src/javascript/package-exported-types.ts +60 -74
- package/src/javascript/parser.ts +10 -3
- package/src/javascript/recipes/change-import.ts +4 -7
- package/src/javascript/style.ts +3 -11
- package/src/javascript/templating/bindings.ts +129 -0
- package/src/javascript/templating/comparator.ts +5 -6
- package/src/javascript/templating/engine.ts +82 -44
- package/src/javascript/templating/index.ts +2 -0
- package/src/javascript/templating/pattern.ts +16 -15
- package/src/javascript/templating/placeholder-replacement.ts +16 -5
- package/src/javascript/templating/rewrite.ts +16 -18
- package/src/javascript/templating/template.ts +45 -3
- package/src/javascript/templating/types.ts +53 -1
- package/src/javascript/templating/utils.ts +17 -0
- package/src/javascript/type-mapping.ts +54 -2
- package/src/style.ts +5 -0
- package/src/test/rewrite-test.ts +61 -3
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import {J, Type} from '../../java';
|
|
17
|
+
import {JavaScriptVisitor} from '../visitor';
|
|
18
|
+
import {Cursor} from '../../tree';
|
|
19
|
+
import {ModuleBinding} from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether the template's dependencies bring a workspace that could resolve `module`.
|
|
23
|
+
*/
|
|
24
|
+
export function isResolvable(module: string, dependencies: Record<string, string>): boolean {
|
|
25
|
+
const segments = module.split('/');
|
|
26
|
+
const pkg = module.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
|
|
27
|
+
return Object.prototype.hasOwnProperty.call(dependencies, pkg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What a declared binding's name is parsed against, ahead of the template and so out of its output.
|
|
32
|
+
* An import is what carries attribution, and costs a module resolution to get it; a declaration
|
|
33
|
+
* names the binding for a module no workspace could have resolved anyway.
|
|
34
|
+
*/
|
|
35
|
+
export function bindingContextStatement(name: string, binding: ModuleBinding, dependencies: Record<string, string>): string {
|
|
36
|
+
if (!isResolvable(binding.module, dependencies)) {
|
|
37
|
+
return binding.typeOnly ? `type ${name} = any;` : `declare const ${name}: any;`;
|
|
38
|
+
}
|
|
39
|
+
const type = binding.typeOnly ? 'type ' : '';
|
|
40
|
+
if (binding.member === '*') {
|
|
41
|
+
return `import ${type}* as ${name} from '${binding.module}';`;
|
|
42
|
+
}
|
|
43
|
+
if (binding.member === undefined || binding.member === 'default') {
|
|
44
|
+
return `import ${type}${name} from '${binding.module}';`;
|
|
45
|
+
}
|
|
46
|
+
const specifier = binding.member === name ? name : `${binding.member} as ${name}`;
|
|
47
|
+
return `import ${type}{${specifier}} from '${binding.module}';`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Renames the identifiers a template uses for its declared bindings to the names the file
|
|
52
|
+
* actually binds. Runs before parameter substitution, so only the template's own code is in
|
|
53
|
+
* scope and a caller's captured code is never rewritten.
|
|
54
|
+
*/
|
|
55
|
+
export async function renameBindings<T extends J>(tree: T, renames: Record<string, string>, modules: Record<string, string>): Promise<T> {
|
|
56
|
+
return new RenameBindingsVisitor(renames, modules).visit(tree, undefined) as Promise<T>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class RenameBindingsVisitor extends JavaScriptVisitor<undefined> {
|
|
60
|
+
constructor(private readonly renames: Record<string, string>,
|
|
61
|
+
private readonly modules: Record<string, string>) {
|
|
62
|
+
super();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override async visitIdentifier(identifier: J.Identifier, p: undefined): Promise<J | undefined> {
|
|
66
|
+
const renamed = this.renames[identifier.simpleName];
|
|
67
|
+
if (renamed === undefined || renamed === identifier.simpleName) {
|
|
68
|
+
return identifier;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Attribution settles it where the context import resolved. It is absent for a module the
|
|
72
|
+
// parse could not reach, and the identifier's position decides instead.
|
|
73
|
+
const resolved = resolvedModule(identifier);
|
|
74
|
+
const refersToBinding = resolved !== undefined
|
|
75
|
+
? resolved === this.modules[identifier.simpleName]
|
|
76
|
+
: !namesItsParent(this.cursor, identifier);
|
|
77
|
+
|
|
78
|
+
return refersToBinding ? {...identifier, simpleName: renamed} as J.Identifier : identifier;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether the parent is naming this identifier — as a property, a method, a declaration — rather than referencing it. */
|
|
83
|
+
function namesItsParent(cursor: Cursor, identifier: J.Identifier): boolean {
|
|
84
|
+
let c: Cursor | undefined = cursor.parent;
|
|
85
|
+
while (c && isPadding(c.value)) {
|
|
86
|
+
c = c.parent;
|
|
87
|
+
}
|
|
88
|
+
const parent = c?.value as { kind?: string; name?: unknown; select?: unknown } | undefined;
|
|
89
|
+
|
|
90
|
+
// A call names a member of whatever it selects from. With nothing selected there is no member,
|
|
91
|
+
// and its `name` is a reference to the function being called.
|
|
92
|
+
if (parent?.kind === J.Kind.MethodInvocation && !parent.select) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const name = parent?.name;
|
|
97
|
+
return name === identifier || (name as { element?: unknown } | undefined)?.element === identifier;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPadding(value: unknown): boolean {
|
|
101
|
+
const kind = (value as { kind?: string } | undefined)?.kind;
|
|
102
|
+
return kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded || kind === J.Kind.Container;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The module an identifier's attribution traces back to, following the owning-class chain to its root. */
|
|
106
|
+
function resolvedModule(identifier: J.Identifier): string | undefined {
|
|
107
|
+
const fieldType = identifier.fieldType;
|
|
108
|
+
if (fieldType?.kind === Type.Kind.Variable) {
|
|
109
|
+
const owner = (fieldType as Type.Variable).owner;
|
|
110
|
+
return owner && Type.isClass(owner) ? rootName(owner as Type.Class) : undefined;
|
|
111
|
+
}
|
|
112
|
+
const type = identifier.type;
|
|
113
|
+
if (type && Type.isMethod(type)) {
|
|
114
|
+
const declaring = (type as Type.Method).declaringType;
|
|
115
|
+
return declaring ? rootName(declaring as Type.Class) : undefined;
|
|
116
|
+
}
|
|
117
|
+
if (type && Type.isClass(type)) {
|
|
118
|
+
return rootName(type as Type.Class);
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rootName(classType: Type.Class): string {
|
|
124
|
+
let current: Type.Class = classType;
|
|
125
|
+
while (current.owningClass && Type.isClass(current.owningClass)) {
|
|
126
|
+
current = current.owningClass as Type.Class;
|
|
127
|
+
}
|
|
128
|
+
return Type.FullyQualified.getFullyQualifiedName(current);
|
|
129
|
+
}
|
|
@@ -344,13 +344,12 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
344
344
|
if (!this.match) return methodInvocation;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
//
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
347
|
+
// A pattern that spells out no type arguments says nothing about them, as with parentheses
|
|
348
|
+
if (methodInvocation.typeParameters) {
|
|
349
|
+
if (!otherMethodInvocation.typeParameters) {
|
|
350
|
+
return this.structuralMismatch('typeParameters');
|
|
351
|
+
}
|
|
351
352
|
|
|
352
|
-
// Visit typeParameters if present
|
|
353
|
-
if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) {
|
|
354
353
|
if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) {
|
|
355
354
|
return this.arrayLengthMismatch('typeParameters.elements');
|
|
356
355
|
}
|
|
@@ -17,12 +17,13 @@ import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..';
|
|
|
17
17
|
import {emptySpace, J, Statement, Type} from '../../java';
|
|
18
18
|
import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..';
|
|
19
19
|
import {create as produce} from 'mutative';
|
|
20
|
-
import {CaptureMarker, PlaceholderUtils, randomizeIds, retainIds, treeIds, WRAPPER_FUNCTION_NAME} from './utils';
|
|
20
|
+
import {CaptureMarker, dedentTemplate, PlaceholderUtils, randomizeIds, retainIds, treeIds, WRAPPER_FUNCTION_NAME} from './utils';
|
|
21
21
|
import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} from './capture';
|
|
22
22
|
import {PlaceholderReplacementVisitor} from './placeholder-replacement';
|
|
23
23
|
import {maybeParenthesize, parenthesize, requiredPrecedence, startsWithDeclarationToken} from './precedence';
|
|
24
24
|
import {JavaCoordinates} from './template';
|
|
25
25
|
import {maybeAutoFormat} from '../format';
|
|
26
|
+
import {renameBindings} from './bindings';
|
|
26
27
|
import {isExpression, isStatement} from '../parser-utils';
|
|
27
28
|
import {randomId} from '../../uuid';
|
|
28
29
|
import ts from "typescript";
|
|
@@ -82,6 +83,21 @@ let templateSourceFileCache: Map<string, ts.SourceFile> | undefined;
|
|
|
82
83
|
*/
|
|
83
84
|
export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>): void {
|
|
84
85
|
templateSourceFileCache = cache;
|
|
86
|
+
templateParsers.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Every template parses under `template.tsx`, so one parser per workspace carries its program
|
|
90
|
+
// from one compile to the next; see `JavaScriptParser.parse`.
|
|
91
|
+
const templateParsers: Map<string, JavaScriptParser> = new Map();
|
|
92
|
+
|
|
93
|
+
function templateParser(workspaceDir?: string): JavaScriptParser {
|
|
94
|
+
const key = workspaceDir ?? "";
|
|
95
|
+
let parser = templateParsers.get(key);
|
|
96
|
+
if (!parser) {
|
|
97
|
+
parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache});
|
|
98
|
+
templateParsers.set(key, parser);
|
|
99
|
+
}
|
|
100
|
+
return parser;
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
/**
|
|
@@ -147,10 +163,7 @@ class TemplateCache {
|
|
|
147
163
|
|
|
148
164
|
// Parse and cache (workspace only needed during parsing)
|
|
149
165
|
// Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
|
|
150
|
-
const parser =
|
|
151
|
-
relativeTo: workspaceDir,
|
|
152
|
-
sourceFileCache: templateSourceFileCache
|
|
153
|
-
});
|
|
166
|
+
const parser = templateParser(workspaceDir);
|
|
154
167
|
const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
|
|
155
168
|
cu = (await parseGenerator.next()).value as JS.CompilationUnit;
|
|
156
169
|
|
|
@@ -202,7 +215,7 @@ export class TemplateEngine {
|
|
|
202
215
|
dependencies: Record<string, string> = {}
|
|
203
216
|
): Promise<J> {
|
|
204
217
|
// Generate type preamble for captures/parameters with types
|
|
205
|
-
const preamble = TemplateEngine.
|
|
218
|
+
const preamble = TemplateEngine.parameterPreamble(parameters);
|
|
206
219
|
|
|
207
220
|
// Build the template string with parameter placeholders
|
|
208
221
|
const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
|
|
@@ -244,6 +257,9 @@ export class TemplateEngine {
|
|
|
244
257
|
* @param coordinates The coordinates specifying where and how to insert the generated AST
|
|
245
258
|
* @param values Map of capture names to values to replace the parameters with
|
|
246
259
|
* @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
|
|
260
|
+
* @param format Whether to fit the result to where it lands
|
|
261
|
+
* @param renames Local names for the template's declared bindings, keyed as declared
|
|
262
|
+
* @param modules The module each declared binding names, keyed as declared
|
|
247
263
|
* @returns A Promise resolving to the generated AST node
|
|
248
264
|
*/
|
|
249
265
|
static async applyTemplateFromAst(
|
|
@@ -252,7 +268,10 @@ export class TemplateEngine {
|
|
|
252
268
|
cursor: Cursor,
|
|
253
269
|
coordinates: JavaCoordinates,
|
|
254
270
|
values: Pick<Map<string, J>, 'get'> = new Map(),
|
|
255
|
-
wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map()
|
|
271
|
+
wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
|
|
272
|
+
format: boolean = true,
|
|
273
|
+
renames: Record<string, string> = {},
|
|
274
|
+
modules: Record<string, string> = {}
|
|
256
275
|
): Promise<J | undefined> {
|
|
257
276
|
// Create substitutions map for placeholders
|
|
258
277
|
const substitutions = new Map<string, Parameter>();
|
|
@@ -264,9 +283,13 @@ export class TemplateEngine {
|
|
|
264
283
|
// Before substitution, so that ids carried over from the source tree survive this pass
|
|
265
284
|
const fresh = await randomizeIds(ast);
|
|
266
285
|
|
|
286
|
+
const bound = Object.keys(renames).length > 0
|
|
287
|
+
? await renameBindings(fresh.tree as J, renames, modules)
|
|
288
|
+
: fresh.tree;
|
|
289
|
+
|
|
267
290
|
// Unsubstitute placeholders with actual parameter values and match results
|
|
268
291
|
const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap);
|
|
269
|
-
const unsubstitutedAst = (await visitor.visit(
|
|
292
|
+
const unsubstitutedAst = (await visitor.visit(bound, null))!;
|
|
270
293
|
|
|
271
294
|
// An id may only be kept where the node answering to it is leaving the tree, which is the
|
|
272
295
|
// subtree this application replaces. A parameter named twice, or spliced in from somewhere
|
|
@@ -280,7 +303,7 @@ export class TemplateEngine {
|
|
|
280
303
|
const uniqueAst = await retainIds(unsubstitutedAst, retainable);
|
|
281
304
|
|
|
282
305
|
// Apply the template to the current AST
|
|
283
|
-
return new TemplateApplier(cursor, coordinates, uniqueAst).apply();
|
|
306
|
+
return new TemplateApplier(cursor, coordinates, uniqueAst, format).apply();
|
|
284
307
|
}
|
|
285
308
|
|
|
286
309
|
/**
|
|
@@ -289,7 +312,33 @@ export class TemplateEngine {
|
|
|
289
312
|
* @param parameters The parameters
|
|
290
313
|
* @returns Array of preamble statements
|
|
291
314
|
*/
|
|
292
|
-
|
|
315
|
+
/** The declarations that give a capture's placeholder its type while the pattern is parsed. */
|
|
316
|
+
static capturePreamble(captures: (Capture | Any | RawCode)[]): string[] {
|
|
317
|
+
const preamble: string[] = [];
|
|
318
|
+
for (const capture of captures) {
|
|
319
|
+
// Raw code is spliced in as source, so it declares nothing
|
|
320
|
+
if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
325
|
+
const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
|
|
326
|
+
if (captureType) {
|
|
327
|
+
const typeString = typeof captureType === 'string'
|
|
328
|
+
? captureType
|
|
329
|
+
: this.typeToString(captureType);
|
|
330
|
+
// `any` attributes nothing, so a declaration for it would only cost a parse
|
|
331
|
+
if (typeString !== 'any') {
|
|
332
|
+
const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
|
|
333
|
+
preamble.push(`let ${placeholder}: ${typeString};`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return preamble;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The parameter counterpart of {@link capturePreamble}. */
|
|
341
|
+
static parameterPreamble(parameters: Parameter[]): string[] {
|
|
293
342
|
const preamble: string[] = [];
|
|
294
343
|
|
|
295
344
|
for (let i = 0; i < parameters.length; i++) {
|
|
@@ -377,7 +426,7 @@ export class TemplateEngine {
|
|
|
377
426
|
|
|
378
427
|
// Always wrap in function body - let the parser decide what it is,
|
|
379
428
|
// then we'll extract intelligently based on what was parsed
|
|
380
|
-
return `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
|
|
429
|
+
return `function ${WRAPPER_FUNCTION_NAME}() { ${dedentTemplate(result)} }`;
|
|
381
430
|
}
|
|
382
431
|
|
|
383
432
|
/**
|
|
@@ -428,15 +477,14 @@ export class TemplateEngine {
|
|
|
428
477
|
}
|
|
429
478
|
|
|
430
479
|
/**
|
|
431
|
-
* Gets the parsed and extracted pattern tree with
|
|
432
|
-
*
|
|
433
|
-
* functionality on top of the shared template tree generation.
|
|
480
|
+
* Gets the parsed and extracted pattern tree, with placeholder identifiers left bare;
|
|
481
|
+
* `attachCaptureMarkers` binds it to a particular set of captures.
|
|
434
482
|
*
|
|
435
483
|
* @param templateParts The string parts of the template
|
|
436
484
|
* @param captures The captures between the string parts (can include RawCode)
|
|
437
485
|
* @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
|
|
438
486
|
* @param dependencies NPM dependencies for type attribution
|
|
439
|
-
* @returns A Promise resolving to the extracted pattern AST
|
|
487
|
+
* @returns A Promise resolving to the extracted pattern AST
|
|
440
488
|
*/
|
|
441
489
|
static async getPatternTree(
|
|
442
490
|
templateParts: TemplateStringsArray,
|
|
@@ -444,29 +492,7 @@ export class TemplateEngine {
|
|
|
444
492
|
contextStatements: string[] = [],
|
|
445
493
|
dependencies: Record<string, string> = {}
|
|
446
494
|
): Promise<J> {
|
|
447
|
-
|
|
448
|
-
const preamble: string[] = [];
|
|
449
|
-
for (const capture of captures) {
|
|
450
|
-
// Skip raw code - it's not a capture
|
|
451
|
-
if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
456
|
-
const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
|
|
457
|
-
if (captureType) {
|
|
458
|
-
// Convert Type to string if needed
|
|
459
|
-
const typeString = typeof captureType === 'string'
|
|
460
|
-
? captureType
|
|
461
|
-
: this.typeToString(captureType);
|
|
462
|
-
// Only add preamble if we have a concrete type (not 'any')
|
|
463
|
-
if (typeString !== 'any') {
|
|
464
|
-
const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
|
|
465
|
-
preamble.push(`let ${placeholder}: ${typeString};`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
// Don't add preamble declarations without types - they don't provide type attribution
|
|
469
|
-
}
|
|
495
|
+
const preamble = TemplateEngine.capturePreamble(captures);
|
|
470
496
|
|
|
471
497
|
// Build the template string with placeholders for captures and raw code
|
|
472
498
|
let result = '';
|
|
@@ -517,18 +543,25 @@ export class TemplateEngine {
|
|
|
517
543
|
const lastStatement = cu.statements[cu.statements.length - 1].element;
|
|
518
544
|
|
|
519
545
|
// Extract from wrapper using shared utility
|
|
520
|
-
|
|
546
|
+
return PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
|
|
547
|
+
}
|
|
521
548
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Binds a pattern tree to a set of captures by attaching a CaptureMarker, carrying that
|
|
551
|
+
* capture's constraint and variadic options, to each placeholder identifier.
|
|
552
|
+
*/
|
|
553
|
+
static async attachCaptureMarkers(tree: J, captures: (Capture | Any | RawCode)[]): Promise<J> {
|
|
554
|
+
const actualCaptures = captures.filter(c =>
|
|
555
|
+
!(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL]))
|
|
556
|
+
) as (Capture | Any)[];
|
|
557
|
+
return (await new MarkerAttachmentVisitor(actualCaptures).visit(tree, undefined))!;
|
|
525
558
|
}
|
|
526
559
|
}
|
|
527
560
|
|
|
528
561
|
/**
|
|
529
562
|
* Visitor that attaches CaptureMarkers to capture identifiers in pattern ASTs.
|
|
530
563
|
* This allows efficient capture detection without string parsing during matching.
|
|
531
|
-
*
|
|
564
|
+
* Reached through TemplateEngine.attachCaptureMarkers().
|
|
532
565
|
*/
|
|
533
566
|
class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
|
|
534
567
|
constructor(private readonly captures: (Capture | Any)[]) {
|
|
@@ -656,7 +689,8 @@ export class TemplateApplier {
|
|
|
656
689
|
constructor(
|
|
657
690
|
private readonly cursor: Cursor,
|
|
658
691
|
private readonly coordinates: JavaCoordinates,
|
|
659
|
-
private readonly ast: J
|
|
692
|
+
private readonly ast: J,
|
|
693
|
+
private readonly shouldFormat: boolean = true
|
|
660
694
|
) {
|
|
661
695
|
}
|
|
662
696
|
|
|
@@ -738,6 +772,10 @@ export class TemplateApplier {
|
|
|
738
772
|
prefix: originalTree.prefix
|
|
739
773
|
};
|
|
740
774
|
|
|
775
|
+
if (!this.shouldFormat) {
|
|
776
|
+
return {...result, id: resultToUse.id};
|
|
777
|
+
}
|
|
778
|
+
|
|
741
779
|
// Apply auto-formatting to the result
|
|
742
780
|
const formatted =
|
|
743
781
|
await maybeAutoFormat(originalTree, result, null, undefined, this.cursor?.parent);
|
|
@@ -233,27 +233,28 @@ export class Pattern {
|
|
|
233
233
|
const cacheKey = generateCacheKey(
|
|
234
234
|
this.templateParts,
|
|
235
235
|
capturesKey,
|
|
236
|
-
|
|
236
|
+
// A capture's type reaches the parse as a declaration, so it shapes the tree the same
|
|
237
|
+
// way an explicit context statement does and belongs in the key alongside one.
|
|
238
|
+
[...contextStatements, ...TemplateEngine.capturePreamble(this.captures)],
|
|
237
239
|
this._options.dependencies || {}
|
|
238
240
|
);
|
|
239
241
|
|
|
240
242
|
// Level 2: Global cache (fast path - shared with Template)
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
let tree = globalAstCache.get(cacheKey);
|
|
244
|
+
if (!tree) {
|
|
245
|
+
// Level 3: Compute via TemplateEngine (slow path)
|
|
246
|
+
tree = await TemplateEngine.getPatternTree(
|
|
247
|
+
this.templateParts,
|
|
248
|
+
this.captures,
|
|
249
|
+
contextStatements,
|
|
250
|
+
this._options.dependencies || {}
|
|
251
|
+
);
|
|
252
|
+
globalAstCache.set(cacheKey, tree);
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.captures,
|
|
251
|
-
contextStatements,
|
|
252
|
-
this._options.dependencies || {}
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
// Cache in both levels
|
|
256
|
-
globalAstCache.set(cacheKey, result);
|
|
255
|
+
// The key names captures but says nothing about their constraints, so two patterns of the
|
|
256
|
+
// same shape share an entry; markers are attached per instance to keep them apart.
|
|
257
|
+
const result = await TemplateEngine.attachCaptureMarkers(tree, this.captures);
|
|
257
258
|
this._cachedAstPattern = result;
|
|
258
259
|
|
|
259
260
|
return result;
|
|
@@ -82,8 +82,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
82
82
|
return super.visitContainer(container, p);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
const newElements = await this.expandVariadicElements(container.elements, undefined, p);
|
|
85
|
+
// A container's element layout is the author's; a block lays its statements out by indentation
|
|
86
|
+
const newElements = await this.expandVariadicElements(container.elements, undefined, p, true);
|
|
87
87
|
|
|
88
88
|
return produce(container, draft => {
|
|
89
89
|
draft.elements = newElements as any;
|
|
@@ -175,6 +175,12 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** As `mergePrefix`, but a line break the source wrote and the template did not is the source's layout. */
|
|
179
|
+
private mergeElementPrefix(sourcePrefix: J.Space, templatePrefix: J.Space): J.Space {
|
|
180
|
+
return sourcePrefix.whitespace.includes('\n') && !templatePrefix.whitespace.includes('\n') ?
|
|
181
|
+
sourcePrefix : this.mergePrefix(sourcePrefix, templatePrefix);
|
|
182
|
+
}
|
|
183
|
+
|
|
178
184
|
/**
|
|
179
185
|
* Expands variadic placeholders in a list of elements.
|
|
180
186
|
*
|
|
@@ -186,7 +192,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
186
192
|
private async expandVariadicElements(
|
|
187
193
|
elements: J.RightPadded<J>[],
|
|
188
194
|
unwrapElement: (element: J) => J = (e) => e,
|
|
189
|
-
p: any
|
|
195
|
+
p: any,
|
|
196
|
+
ownLayout: boolean = false
|
|
190
197
|
): Promise<J.RightPadded<J>[]> {
|
|
191
198
|
const newElements: J.RightPadded<J>[] = [];
|
|
192
199
|
|
|
@@ -263,7 +270,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
263
270
|
if (i === 0 && draft.element) {
|
|
264
271
|
// Merge the placeholder's prefix with the first item's prefix
|
|
265
272
|
// Modify prefix directly within the draft
|
|
266
|
-
draft.element.prefix =
|
|
273
|
+
draft.element.prefix = ownLayout ?
|
|
274
|
+
this.mergeElementPrefix(draft.element.prefix, element.prefix) :
|
|
275
|
+
this.mergePrefix(draft.element.prefix, element.prefix);
|
|
267
276
|
}
|
|
268
277
|
// Keep all other wrapper properties (including markers with Semicolon)
|
|
269
278
|
}));
|
|
@@ -273,7 +282,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
273
282
|
newElements.push(produce(wrapped, draft => {
|
|
274
283
|
draft.element = produce(elem, itemDraft => {
|
|
275
284
|
if (i === 0) {
|
|
276
|
-
itemDraft.prefix =
|
|
285
|
+
itemDraft.prefix = ownLayout ?
|
|
286
|
+
this.mergeElementPrefix(elem.prefix, element.prefix) :
|
|
287
|
+
this.mergePrefix(elem.prefix, element.prefix);
|
|
277
288
|
}
|
|
278
289
|
// For i > 0, prefix is already correct, no changes needed
|
|
279
290
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {Cursor, ExecutionContext, Recipe, TreeVisitor} from '../..';
|
|
17
17
|
import {J, Statement} from '../../java';
|
|
18
|
-
import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule} from './types';
|
|
18
|
+
import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule, TryOnOptions} from './types';
|
|
19
19
|
import {MatchResult, Pattern} from './pattern';
|
|
20
20
|
import {Template} from './template';
|
|
21
21
|
import {JavaScriptVisitor} from '../visitor';
|
|
@@ -28,11 +28,12 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
28
28
|
private readonly before: Pattern[],
|
|
29
29
|
private readonly after: Template | ((match: MatchResult) => Template),
|
|
30
30
|
private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>,
|
|
31
|
-
private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean
|
|
31
|
+
private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>,
|
|
32
|
+
private readonly format?: boolean
|
|
32
33
|
) {
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
36
|
+
async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
|
|
36
37
|
// Evaluate preMatch before attempting any pattern matching
|
|
37
38
|
if (this.preMatch) {
|
|
38
39
|
const preMatchResult = await this.preMatch(node, { cursor });
|
|
@@ -56,14 +57,10 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
56
57
|
// Apply transformation
|
|
57
58
|
let result: J | undefined;
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} else {
|
|
64
|
-
// Use template.apply() as before
|
|
65
|
-
result = await this.after.apply(node, cursor, { values: match });
|
|
66
|
-
}
|
|
60
|
+
const template = typeof this.after === 'function' ? this.after(match) : this.after;
|
|
61
|
+
const bindings = options?.bindings ?? (options?.visitor && template.resolveBindings(options.visitor));
|
|
62
|
+
result = await template.apply(node, cursor,
|
|
63
|
+
{ values: match, format: this.format, bindings: bindings || undefined });
|
|
67
64
|
|
|
68
65
|
if (result) {
|
|
69
66
|
return result;
|
|
@@ -84,10 +81,10 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
84
81
|
super([], () => undefined as unknown as Template);
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
88
|
-
const firstResult = await first.tryOn(cursor, node);
|
|
84
|
+
async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
|
|
85
|
+
const firstResult = await first.tryOn(cursor, node, options);
|
|
89
86
|
if (firstResult !== undefined) {
|
|
90
|
-
const secondResult = await next.tryOn(cursor, firstResult);
|
|
87
|
+
const secondResult = await next.tryOn(cursor, firstResult, options);
|
|
91
88
|
return secondResult ?? firstResult;
|
|
92
89
|
}
|
|
93
90
|
return undefined;
|
|
@@ -104,12 +101,12 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
104
101
|
super([], () => undefined as unknown as Template);
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
108
|
-
const firstResult = await first.tryOn(cursor, node);
|
|
104
|
+
async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
|
|
105
|
+
const firstResult = await first.tryOn(cursor, node, options);
|
|
109
106
|
if (firstResult !== undefined) {
|
|
110
107
|
return firstResult;
|
|
111
108
|
}
|
|
112
|
-
return await alternative.tryOn(cursor, node);
|
|
109
|
+
return await alternative.tryOn(cursor, node, options);
|
|
113
110
|
}
|
|
114
111
|
})();
|
|
115
112
|
}
|
|
@@ -171,7 +168,8 @@ export function rewrite(
|
|
|
171
168
|
Array.isArray(config.before) ? config.before : [config.before],
|
|
172
169
|
config.after,
|
|
173
170
|
config.preMatch,
|
|
174
|
-
config.postMatch
|
|
171
|
+
config.postMatch,
|
|
172
|
+
config.format
|
|
175
173
|
);
|
|
176
174
|
}
|
|
177
175
|
|