@openrewrite/rewrite 8.91.0 → 8.91.2
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 +96 -43
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +668 -236
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts +138 -0
- package/dist/javascript/amd.d.ts.map +1 -0
- package/dist/javascript/amd.js +768 -0
- package/dist/javascript/amd.js.map +1 -0
- 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/binding.d.ts +94 -0
- package/dist/javascript/binding.d.ts.map +1 -0
- package/dist/javascript/binding.js +335 -0
- package/dist/javascript/binding.js.map +1 -0
- 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/index.d.ts +8 -1
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +26 -1
- package/dist/javascript/index.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-utils.d.ts.map +1 -1
- package/dist/javascript/parser-utils.js +2 -0
- package/dist/javascript/parser-utils.js.map +1 -1
- package/dist/javascript/parser.d.ts +7 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +17 -4
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/change-import.d.ts +2 -4
- package/dist/javascript/recipes/change-import.d.ts.map +1 -1
- package/dist/javascript/recipes/change-import.js +8 -244
- package/dist/javascript/recipes/change-import.js.map +1 -1
- package/dist/javascript/remove-import.d.ts +2 -28
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +19 -57
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/javascript/scope.d.ts +49 -0
- package/dist/javascript/scope.d.ts.map +1 -0
- package/dist/javascript/scope.js +324 -0
- package/dist/javascript/scope.js.map +1 -0
- 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 +8 -0
- package/dist/javascript/templating/bindings.d.ts.map +1 -0
- package/dist/javascript/templating/bindings.js +92 -0
- package/dist/javascript/templating/bindings.js.map +1 -0
- package/dist/javascript/templating/comparator.d.ts +10 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +48 -20
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +34 -10
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +109 -55
- 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 +4 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +24 -10
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/precedence.d.ts +12 -4
- package/dist/javascript/templating/precedence.d.ts.map +1 -1
- package/dist/javascript/templating/precedence.js +18 -11
- package/dist/javascript/templating/precedence.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +19 -17
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +14 -0
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +63 -3
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +39 -1
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +6 -1
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +20 -2
- 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 +2 -2
- package/src/java/formatting-utils.ts +14 -10
- package/src/java/type.ts +5 -0
- package/src/javascript/add-import.ts +751 -247
- package/src/javascript/amd.ts +915 -0
- package/src/javascript/assertions.ts +33 -5
- package/src/javascript/binding.ts +449 -0
- 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/index.ts +12 -1
- package/src/javascript/node-resolution-result.ts +59 -4
- package/src/javascript/package-exported-types.ts +60 -74
- package/src/javascript/parser-utils.ts +2 -0
- package/src/javascript/parser.ts +23 -3
- package/src/javascript/recipes/change-import.ts +10 -263
- package/src/javascript/remove-import.ts +20 -56
- package/src/javascript/scope.ts +338 -0
- package/src/javascript/style.ts +3 -11
- package/src/javascript/templating/bindings.ts +99 -0
- package/src/javascript/templating/comparator.ts +57 -20
- package/src/javascript/templating/engine.ts +148 -68
- package/src/javascript/templating/index.ts +1 -0
- package/src/javascript/templating/pattern.ts +19 -16
- package/src/javascript/templating/placeholder-replacement.ts +32 -11
- package/src/javascript/templating/precedence.ts +21 -13
- package/src/javascript/templating/rewrite.ts +23 -17
- package/src/javascript/templating/template.ts +84 -9
- package/src/javascript/templating/types.ts +46 -1
- package/src/javascript/templating/utils.ts +21 -2
- package/src/javascript/type-mapping.ts +54 -2
- package/src/style.ts +5 -0
- package/src/test/rewrite-test.ts +61 -3
|
@@ -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,18 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
56
57
|
// Apply transformation
|
|
57
58
|
let result: J | undefined;
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
const template = typeof this.after === 'function' ? this.after(match) : this.after;
|
|
61
|
+
const bindings = options?.bindings ??
|
|
62
|
+
(options?.visitor ? await template.resolveBindings(options.visitor) : undefined);
|
|
63
|
+
// Applying without them would splice the context's own names in unbound, which
|
|
64
|
+
// reads as a working edit and is not one.
|
|
65
|
+
if (bindings === undefined && await template.bindsModules()) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Template binds modules in its context, so applying it needs their local names. " +
|
|
68
|
+
"Pass {visitor: this} to tryOn, or bindings you resolved yourself.");
|
|
66
69
|
}
|
|
70
|
+
result = await template.apply(node, cursor,
|
|
71
|
+
{ values: match, format: this.format, bindings: bindings || undefined });
|
|
67
72
|
|
|
68
73
|
if (result) {
|
|
69
74
|
return result;
|
|
@@ -84,10 +89,10 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
84
89
|
super([], () => undefined as unknown as Template);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
88
|
-
const firstResult = await first.tryOn(cursor, node);
|
|
92
|
+
async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
|
|
93
|
+
const firstResult = await first.tryOn(cursor, node, options);
|
|
89
94
|
if (firstResult !== undefined) {
|
|
90
|
-
const secondResult = await next.tryOn(cursor, firstResult);
|
|
95
|
+
const secondResult = await next.tryOn(cursor, firstResult, options);
|
|
91
96
|
return secondResult ?? firstResult;
|
|
92
97
|
}
|
|
93
98
|
return undefined;
|
|
@@ -104,12 +109,12 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
104
109
|
super([], () => undefined as unknown as Template);
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
108
|
-
const firstResult = await first.tryOn(cursor, node);
|
|
112
|
+
async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
|
|
113
|
+
const firstResult = await first.tryOn(cursor, node, options);
|
|
109
114
|
if (firstResult !== undefined) {
|
|
110
115
|
return firstResult;
|
|
111
116
|
}
|
|
112
|
-
return await alternative.tryOn(cursor, node);
|
|
117
|
+
return await alternative.tryOn(cursor, node, options);
|
|
113
118
|
}
|
|
114
119
|
})();
|
|
115
120
|
}
|
|
@@ -171,7 +176,8 @@ export function rewrite(
|
|
|
171
176
|
Array.isArray(config.before) ? config.before : [config.before],
|
|
172
177
|
config.after,
|
|
173
178
|
config.preMatch,
|
|
174
|
-
config.postMatch
|
|
179
|
+
config.postMatch,
|
|
180
|
+
config.format
|
|
175
181
|
);
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
import {Cursor, Tree} from '../..';
|
|
17
17
|
import {J} from '../../java';
|
|
18
18
|
import {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types';
|
|
19
|
+
import {maybeBind} from '../binding';
|
|
20
|
+
import {ContextBinding} from './engine';
|
|
21
|
+
import {JavaScriptVisitor} from '../visitor';
|
|
19
22
|
import {MatchResult} from './pattern';
|
|
20
23
|
import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
21
24
|
import {CAPTURE_NAME_SYMBOL, RAW_CODE_SYMBOL} from './capture';
|
|
@@ -174,6 +177,7 @@ export class TemplateBuilder {
|
|
|
174
177
|
export class Template {
|
|
175
178
|
private options: TemplateOptions = {};
|
|
176
179
|
private _cachedTemplate?: J;
|
|
180
|
+
private _contextBindings?: Promise<ContextBinding[]>;
|
|
177
181
|
|
|
178
182
|
/**
|
|
179
183
|
* Creates a new template.
|
|
@@ -221,6 +225,7 @@ export class Template {
|
|
|
221
225
|
this.options = {...this.options, ...options};
|
|
222
226
|
// Invalidate cache when configuration changes
|
|
223
227
|
this._cachedTemplate = undefined;
|
|
228
|
+
this._contextBindings = undefined;
|
|
224
229
|
return this;
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -237,10 +242,10 @@ export class Template {
|
|
|
237
242
|
* @returns The cached or newly computed template tree
|
|
238
243
|
* @internal
|
|
239
244
|
*/
|
|
240
|
-
private async getTemplateTree(): Promise<
|
|
245
|
+
private async getTemplateTree(): Promise<J> {
|
|
241
246
|
// Level 1: Instance cache (fastest path)
|
|
242
247
|
if (this._cachedTemplate) {
|
|
243
|
-
return this._cachedTemplate
|
|
248
|
+
return this._cachedTemplate;
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
// Generate cache key for global lookup
|
|
@@ -258,15 +263,17 @@ export class Template {
|
|
|
258
263
|
const cacheKey = generateCacheKey(
|
|
259
264
|
this.templateParts,
|
|
260
265
|
parametersKey,
|
|
261
|
-
|
|
262
|
-
this.
|
|
266
|
+
// As in Pattern.getAstPattern: a parameter's type reaches the parse as a declaration
|
|
267
|
+
[...contextStatements, ...TemplateEngine.parameterPreamble(this.parameters)],
|
|
268
|
+
this.options.dependencies || {},
|
|
269
|
+
this.options.types
|
|
263
270
|
);
|
|
264
271
|
|
|
265
272
|
// Level 2: Global cache (fast path - shared with Pattern)
|
|
266
273
|
const cached = globalAstCache.get(cacheKey);
|
|
267
274
|
if (cached) {
|
|
268
|
-
this._cachedTemplate = cached
|
|
269
|
-
return cached
|
|
275
|
+
this._cachedTemplate = cached;
|
|
276
|
+
return cached;
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
// Level 3: Compute via TemplateEngine (slow path)
|
|
@@ -274,8 +281,9 @@ export class Template {
|
|
|
274
281
|
this.templateParts,
|
|
275
282
|
this.parameters,
|
|
276
283
|
contextStatements,
|
|
277
|
-
this.options.dependencies || {}
|
|
278
|
-
|
|
284
|
+
this.options.dependencies || {},
|
|
285
|
+
this.options.types
|
|
286
|
+
);
|
|
279
287
|
|
|
280
288
|
// Cache in both levels
|
|
281
289
|
globalAstCache.set(cacheKey, result);
|
|
@@ -284,6 +292,53 @@ export class Template {
|
|
|
284
292
|
return result;
|
|
285
293
|
}
|
|
286
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Binds every module this template declares in the file `visitor` is traversing, and returns
|
|
297
|
+
* the local names to hand back through {@link ApplyOptions.bindings}. A module the dependencies
|
|
298
|
+
* cannot resolve is bound whether or not the template goes on to reference it, so call this
|
|
299
|
+
* where the template is known to apply — {@link RewriteRule.tryOn} does, once a pattern matched.
|
|
300
|
+
*/
|
|
301
|
+
async resolveBindings(visitor: JavaScriptVisitor<any>): Promise<Record<string, string>> {
|
|
302
|
+
const resolved: Record<string, string> = {};
|
|
303
|
+
for (const binding of await this.contextBindings()) {
|
|
304
|
+
// Recognising the reference the template splices in takes attribution, so a module the
|
|
305
|
+
// workspace could not resolve is bound whether or not the template turns out to use it.
|
|
306
|
+
const onlyIfReferenced = binding.attributed;
|
|
307
|
+
const bound = maybeBind(visitor, {
|
|
308
|
+
module: binding.module!,
|
|
309
|
+
member: binding.member,
|
|
310
|
+
typeOnly: binding.typeOnly,
|
|
311
|
+
preferredName: binding.name,
|
|
312
|
+
onlyIfReferenced
|
|
313
|
+
});
|
|
314
|
+
// An unresolved binding is left out rather than recorded as `undefined`, so `apply()`'s
|
|
315
|
+
// own "applied without a local name" check catches it, same as a caller-omitted one.
|
|
316
|
+
if (bound !== undefined) {
|
|
317
|
+
resolved[binding.name] = bound;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return resolved;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Whether the context binds a module, which applying this template therefore has to resolve. */
|
|
324
|
+
async bindsModules(): Promise<boolean> {
|
|
325
|
+
return (await this.contextBindings()).length > 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** What this template's context statements bind, which is what it needs bound in the target file. */
|
|
329
|
+
private contextBindings(): Promise<ContextBinding[]> {
|
|
330
|
+
return this._contextBindings ??= this.deriveContextBindings();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private deriveContextBindings(): Promise<ContextBinding[]> {
|
|
334
|
+
return TemplateEngine.getContextBindings(
|
|
335
|
+
this.templateParts, this.parameters,
|
|
336
|
+
this.options.context || this.options.imports || [],
|
|
337
|
+
this.options.dependencies || {},
|
|
338
|
+
this.options.types
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
287
342
|
/**
|
|
288
343
|
* Applies this template and returns the resulting tree.
|
|
289
344
|
*
|
|
@@ -346,6 +401,23 @@ export class Template {
|
|
|
346
401
|
}
|
|
347
402
|
}
|
|
348
403
|
|
|
404
|
+
const renames: Record<string, string> = {};
|
|
405
|
+
const modules: Record<string, string> = {};
|
|
406
|
+
// Supplying names is what asks for the context's modules to be bound in the file being
|
|
407
|
+
// edited; without them a context import only types the template.
|
|
408
|
+
if (options?.bindings !== undefined) {
|
|
409
|
+
for (const binding of await this.contextBindings()) {
|
|
410
|
+
const bound = options.bindings[binding.name];
|
|
411
|
+
if (bound === undefined) {
|
|
412
|
+
throw new Error(`Template binds '${binding.module}' in its context, but no local name was ` +
|
|
413
|
+
`given for '${binding.name}'. Either pass bindings from resolveBindings(visitor), or — if it ` +
|
|
414
|
+
`already did — binding was refused, which an AMD block or a file requiring its modules can do.`);
|
|
415
|
+
}
|
|
416
|
+
renames[binding.name] = bound;
|
|
417
|
+
modules[binding.name] = binding.module!;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
349
421
|
// Use instance-level cache to get the template tree
|
|
350
422
|
const ast = await this.getTemplateTree();
|
|
351
423
|
|
|
@@ -359,7 +431,10 @@ export class Template {
|
|
|
359
431
|
mode: JavaCoordinates.Mode.Replace
|
|
360
432
|
},
|
|
361
433
|
normalizedValues,
|
|
362
|
-
wrappersMap
|
|
434
|
+
wrappersMap,
|
|
435
|
+
options?.format ?? true,
|
|
436
|
+
renames,
|
|
437
|
+
modules
|
|
363
438
|
);
|
|
364
439
|
}
|
|
365
440
|
}
|
|
@@ -18,6 +18,7 @@ import {J, Type} from '../../java';
|
|
|
18
18
|
import type {Pattern} from "./pattern";
|
|
19
19
|
import type {Template} from "./template";
|
|
20
20
|
import type {CaptureValue, RawCode} from "./capture";
|
|
21
|
+
import type {JavaScriptVisitor} from "../visitor";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Options for variadic captures that match zero or more nodes in a sequence.
|
|
@@ -348,6 +349,12 @@ export interface PatternOptions {
|
|
|
348
349
|
*/
|
|
349
350
|
dependencies?: Record<string, string>;
|
|
350
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Type packages to load whose declarations nothing imports, as {@link TemplateOptions.types}.
|
|
354
|
+
* A pattern matches on attribution, so a module typed only ambiently needs this to match.
|
|
355
|
+
*/
|
|
356
|
+
types?: string[];
|
|
357
|
+
|
|
351
358
|
/**
|
|
352
359
|
* When true, allows patterns without type annotations to match code with type annotations.
|
|
353
360
|
* This enables more flexible pattern matching during development or when full type attribution
|
|
@@ -481,6 +488,15 @@ export interface TemplateOptions {
|
|
|
481
488
|
* The template engine will create a package.json with these dependencies.
|
|
482
489
|
*/
|
|
483
490
|
dependencies?: Record<string, string>;
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Type packages to load whose declarations nothing imports. TypeScript reads `@types/*` on its
|
|
494
|
+
* own and everything else only when named here, so a package declaring its modules ambiently —
|
|
495
|
+
* rather than at a path matching the specifier — resolves only with this set.
|
|
496
|
+
*
|
|
497
|
+
* @example `{dependencies: {'@sapui5/types': '^1.120.0'}, types: ['@sapui5/types']}`
|
|
498
|
+
*/
|
|
499
|
+
types?: string[];
|
|
484
500
|
}
|
|
485
501
|
|
|
486
502
|
/**
|
|
@@ -509,6 +525,32 @@ export interface ApplyOptions {
|
|
|
509
525
|
* ```
|
|
510
526
|
*/
|
|
511
527
|
values?: Map<Capture | string, J> | MatchResult | Record<string, J>;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Whether the result is fitted to where it lands. Defaults to `true`; pass `false` to assemble
|
|
531
|
+
* several results into one subtree and format that subtree once, rather than once per application.
|
|
532
|
+
* The anchor supplies the result's prefix either way.
|
|
533
|
+
*/
|
|
534
|
+
format?: boolean;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Local names for the bindings the template declares, keyed as they are declared — normally
|
|
538
|
+
* {@link Template.resolveBindings}. Every declared binding needs one.
|
|
539
|
+
*/
|
|
540
|
+
bindings?: Record<string, string>;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Options a caller supplies per node, as against the ones {@link RewriteConfig} fixes for the rule. */
|
|
544
|
+
export interface TryOnOptions {
|
|
545
|
+
/**
|
|
546
|
+
* The visitor to bind the applied template's declared modules in. The rule holds that template,
|
|
547
|
+
* so it resolves them itself, and only once a pattern has matched — a rule that does not fire
|
|
548
|
+
* leaves the file's imports alone. {@link TryOnOptions.bindings} overrides this.
|
|
549
|
+
*/
|
|
550
|
+
visitor?: JavaScriptVisitor<any>;
|
|
551
|
+
|
|
552
|
+
/** As {@link ApplyOptions.bindings}, for a caller that resolves the modules some other way. */
|
|
553
|
+
bindings?: Record<string, string>;
|
|
512
554
|
}
|
|
513
555
|
|
|
514
556
|
/**
|
|
@@ -524,7 +566,7 @@ export interface RewriteRule {
|
|
|
524
566
|
* When using in a visitor, always use the `|| node` pattern to return the original
|
|
525
567
|
* node when there's no match: `return await rule.tryOn(this.cursor, node) || node;`
|
|
526
568
|
*/
|
|
527
|
-
tryOn(cursor: Cursor, node: J): Promise<J | undefined>;
|
|
569
|
+
tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined>;
|
|
528
570
|
|
|
529
571
|
/**
|
|
530
572
|
* Chains this rule with another rule, creating a composite rule that applies both transformations sequentially.
|
|
@@ -624,6 +666,9 @@ export interface RewriteConfig {
|
|
|
624
666
|
before: Pattern | Pattern[];
|
|
625
667
|
after: Template | ((match: MatchResult) => Template);
|
|
626
668
|
|
|
669
|
+
/** As {@link ApplyOptions.format}, applied to every node the rule rewrites. */
|
|
670
|
+
format?: boolean;
|
|
671
|
+
|
|
627
672
|
/**
|
|
628
673
|
* Optional predicate evaluated BEFORE pattern matching.
|
|
629
674
|
* Use for efficient early filtering based on AST context when captures aren't needed.
|
|
@@ -107,13 +107,15 @@ export function generateCacheKey(
|
|
|
107
107
|
templateParts: string[] | TemplateStringsArray,
|
|
108
108
|
itemsKey: string,
|
|
109
109
|
contextStatements: string[],
|
|
110
|
-
dependencies: Record<string, string
|
|
110
|
+
dependencies: Record<string, string>,
|
|
111
|
+
types?: string[]
|
|
111
112
|
): string {
|
|
112
113
|
return [
|
|
113
114
|
Array.from(templateParts).join('|'),
|
|
114
115
|
itemsKey,
|
|
115
116
|
contextStatements.join(';'),
|
|
116
|
-
JSON.stringify(dependencies)
|
|
117
|
+
JSON.stringify(dependencies),
|
|
118
|
+
JSON.stringify(types ?? null)
|
|
117
119
|
].join('::');
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -182,6 +184,23 @@ export function retainIds<T extends J>(tree: T, retainable: ReadonlySet<string>)
|
|
|
182
184
|
return new RetainIdVisitor(retainable).visit(tree, null) as Promise<T>;
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Strips the indentation a template carries from the recipe source it was written in. Its first
|
|
189
|
+
* line starts at the opening backtick, so the common indent is the one shared by the lines below.
|
|
190
|
+
*/
|
|
191
|
+
export function dedentTemplate(code: string): string {
|
|
192
|
+
const lines = code.split("\n");
|
|
193
|
+
let indent = Infinity;
|
|
194
|
+
for (const line of lines.slice(1)) {
|
|
195
|
+
if (line.trim().length > 0) {
|
|
196
|
+
indent = Math.min(indent, line.length - line.trimStart().length);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return indent === Infinity || indent === 0 ?
|
|
200
|
+
code :
|
|
201
|
+
[lines[0], ...lines.slice(1).map(line => line.slice(indent))].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
/**
|
|
186
205
|
* Marker that stores capture metadata on pattern AST nodes.
|
|
187
206
|
* This avoids the need to parse capture names from identifiers during matching.
|
|
@@ -17,6 +17,7 @@ import ts from "typescript";
|
|
|
17
17
|
import * as path from "path";
|
|
18
18
|
import {Type} from "../java";
|
|
19
19
|
import FUNCTION_TYPE_NAME = Type.FUNCTION_TYPE_NAME;
|
|
20
|
+
import OBJECT_TYPE_NAME = Type.OBJECT_TYPE_NAME;
|
|
20
21
|
|
|
21
22
|
export class JavaScriptTypeMapping {
|
|
22
23
|
// Primary cache: Use type signatures (preferring type.id) as cache keys
|
|
@@ -461,14 +462,25 @@ export class JavaScriptTypeMapping {
|
|
|
461
462
|
return functionType;
|
|
462
463
|
}
|
|
463
464
|
|
|
464
|
-
//
|
|
465
|
+
// A structural type has no nominal name, so every object literal `{a: 1}` and type
|
|
466
|
+
// literal `{a: number}` shares the synthetic FQN `{}`, with `members` carrying the shape.
|
|
465
467
|
if (type.flags & ts.TypeFlags.Object) {
|
|
466
468
|
const objectFlags = (type as ts.ObjectType).objectFlags;
|
|
467
469
|
if (objectFlags & ts.ObjectFlags.Anonymous) {
|
|
468
|
-
|
|
470
|
+
const objectType = this.createEmptyObjectType();
|
|
471
|
+
this.typeCache.set(signature, objectType);
|
|
472
|
+
this.populateClassType(objectType, type);
|
|
473
|
+
return objectType;
|
|
469
474
|
}
|
|
470
475
|
}
|
|
471
476
|
|
|
477
|
+
// The `object` keyword: an object of unknown shape.
|
|
478
|
+
if (type.flags & ts.TypeFlags.NonPrimitive) {
|
|
479
|
+
const objectType = this.createEmptyObjectType();
|
|
480
|
+
this.typeCache.set(signature, objectType);
|
|
481
|
+
return objectType;
|
|
482
|
+
}
|
|
483
|
+
|
|
472
484
|
// Pre-cache as unknownType before resolving type aliases to prevent infinite
|
|
473
485
|
// recursion when alias resolution cycles back through getType with different type.ids
|
|
474
486
|
this.typeCache.set(signature, Type.unknownType);
|
|
@@ -820,6 +832,29 @@ export class JavaScriptTypeMapping {
|
|
|
820
832
|
}
|
|
821
833
|
|
|
822
834
|
methodType(node: ts.Node): Type.Method | undefined {
|
|
835
|
+
// An object literal is a `J.NewClass`, and `J.NewClass.getType()` reads
|
|
836
|
+
// `constructorType.returnType`, as it does for a Java anonymous class.
|
|
837
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
838
|
+
const objectType = this.type(node);
|
|
839
|
+
if (!Type.isFullyQualified(objectType)) {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
kind: Type.Kind.Method,
|
|
844
|
+
flags: 0,
|
|
845
|
+
declaringType: objectType,
|
|
846
|
+
name: '<constructor>',
|
|
847
|
+
returnType: objectType,
|
|
848
|
+
parameterNames: [],
|
|
849
|
+
parameterTypes: [],
|
|
850
|
+
thrownExceptions: [],
|
|
851
|
+
annotations: [],
|
|
852
|
+
declaredFormalTypeNames: [],
|
|
853
|
+
toJSON: function () {
|
|
854
|
+
return Type.signature(this);
|
|
855
|
+
}
|
|
856
|
+
} as Type.Method;
|
|
857
|
+
}
|
|
823
858
|
|
|
824
859
|
let signature: ts.Signature | undefined;
|
|
825
860
|
let methodName: string;
|
|
@@ -1644,6 +1679,23 @@ export class JavaScriptTypeMapping {
|
|
|
1644
1679
|
return gtv;
|
|
1645
1680
|
}
|
|
1646
1681
|
|
|
1682
|
+
private createEmptyObjectType(): Type.Class {
|
|
1683
|
+
return {
|
|
1684
|
+
kind: Type.Kind.Class,
|
|
1685
|
+
flags: 0,
|
|
1686
|
+
classKind: Type.Class.Kind.Interface,
|
|
1687
|
+
fullyQualifiedName: OBJECT_TYPE_NAME,
|
|
1688
|
+
typeParameters: [],
|
|
1689
|
+
annotations: [],
|
|
1690
|
+
interfaces: [],
|
|
1691
|
+
members: [],
|
|
1692
|
+
methods: [],
|
|
1693
|
+
toJSON: function () {
|
|
1694
|
+
return Type.signature(this);
|
|
1695
|
+
}
|
|
1696
|
+
} as Type.Class;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1647
1699
|
/**
|
|
1648
1700
|
* Create an empty function type shell with FQN 𝑓.
|
|
1649
1701
|
* The shell will be populated later to handle circular references.
|
package/src/style.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface Style {
|
|
|
20
20
|
readonly kind: string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Style markers carry their own kind (e.g. Autodetect, PrettierStyle), so `styles` is what identifies one. */
|
|
24
|
+
export function isNamedStyles(marker: Marker): marker is NamedStyles<string> {
|
|
25
|
+
return Array.isArray((marker as Partial<NamedStyles<string>>).styles);
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export interface NamedStyles<K extends string = typeof MarkersKind.NamedStyles> extends Marker {
|
|
24
29
|
readonly kind: K
|
|
25
30
|
readonly name: string;
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -20,7 +20,6 @@ import {Parser} from "../parser";
|
|
|
20
20
|
import {TreePrinters} from "../print";
|
|
21
21
|
import {SourceFile} from "../tree";
|
|
22
22
|
import {Result, scheduleRun} from "../run";
|
|
23
|
-
import {SnowflakeId} from "@akashrajpurohit/snowflake-id";
|
|
24
23
|
import {mapAsync, trimIndent} from "../util";
|
|
25
24
|
import {ParseErrorKind} from "../parse-error";
|
|
26
25
|
import {MarkersKind, ParseExceptionResult} from "../markers";
|
|
@@ -38,6 +37,15 @@ export interface SourceSpec<T extends SourceFile> {
|
|
|
38
37
|
ext: string
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
/** The fields a test configures on a spec, as opposed to the machinery it runs with. */
|
|
41
|
+
interface RecipeSpecConfiguration {
|
|
42
|
+
checkParsePrintIdempotence: boolean;
|
|
43
|
+
allowEmptyDiff: boolean;
|
|
44
|
+
recipe: Recipe;
|
|
45
|
+
recipeExecutionContext: ExecutionContext;
|
|
46
|
+
dataTableAssertions: { [key: string]: (rows: any[]) => void };
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export class RecipeSpec {
|
|
42
50
|
checkParsePrintIdempotence: boolean = true
|
|
43
51
|
allowEmptyDiff: boolean = false
|
|
@@ -57,10 +65,58 @@ export class RecipeSpec {
|
|
|
57
65
|
|
|
58
66
|
private dataTableAssertions: { [key: string]: (rows: any[]) => void } = {}
|
|
59
67
|
|
|
68
|
+
// A suite typically declares one spec and configures it per test, which makes the spec a
|
|
69
|
+
// channel between tests unless each test starts from the same configuration. A consumer opts
|
|
70
|
+
// into that reset by calling `trackSuiteConfiguration` from a vitest setup file, as
|
|
71
|
+
// `test/setup.ts` does; until it does, `live` is undefined and no spec is retained here.
|
|
72
|
+
private static live?: RecipeSpec[];
|
|
73
|
+
private suiteConfiguration?: RecipeSpecConfiguration;
|
|
74
|
+
|
|
75
|
+
constructor() {
|
|
76
|
+
RecipeSpec.live?.push(this);
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
dataTable<Row>(name: string, allRows: (rows: Row[]) => void) {
|
|
61
80
|
this.dataTableAssertions[name] = allRows;
|
|
62
81
|
}
|
|
63
82
|
|
|
83
|
+
private configuration(): RecipeSpecConfiguration {
|
|
84
|
+
return {
|
|
85
|
+
checkParsePrintIdempotence: this.checkParsePrintIdempotence,
|
|
86
|
+
allowEmptyDiff: this.allowEmptyDiff,
|
|
87
|
+
recipe: this.recipe,
|
|
88
|
+
recipeExecutionContext: this.recipeExecutionContext,
|
|
89
|
+
dataTableAssertions: {...this.dataTableAssertions}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Starts recording specs, so that the calls below have something to act on. */
|
|
94
|
+
static trackSuiteConfiguration(): void {
|
|
95
|
+
RecipeSpec.live = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Records the configuration each test in the current suite starts from. */
|
|
99
|
+
static captureSuiteConfiguration(): void {
|
|
100
|
+
for (const spec of RecipeSpec.live ?? []) {
|
|
101
|
+
spec.suiteConfiguration = spec.configuration();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Undoes whatever the previous test configured. */
|
|
106
|
+
static restoreSuiteConfiguration(): void {
|
|
107
|
+
for (const spec of RecipeSpec.live ?? []) {
|
|
108
|
+
if (spec.suiteConfiguration) {
|
|
109
|
+
Object.assign(spec, spec.suiteConfiguration);
|
|
110
|
+
spec.dataTableAssertions = {...spec.suiteConfiguration.dataTableAssertions};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Bounds the registry to the suite that is running, which is all it has to span. */
|
|
116
|
+
static forgetSuiteSpecs(): void {
|
|
117
|
+
RecipeSpec.live &&= [];
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
async rewriteRun(...sourceSpecs: (SourceSpec<any> | Generator<SourceSpec<any>, void, unknown> | AsyncGenerator<SourceSpec<any>, void, unknown>)[]): Promise<void> {
|
|
65
121
|
// Flatten generators into a list of sourceSpecs
|
|
66
122
|
const flattenedSpecs: SourceSpec<any>[] = [];
|
|
@@ -230,11 +286,13 @@ export class RecipeSpec {
|
|
|
230
286
|
* Parse the whole group together so the sources can reference once another.
|
|
231
287
|
*/
|
|
232
288
|
private async parse(specs: SourceSpec<any>[]): Promise<[SourceSpec<any>, SourceFile][]> {
|
|
233
|
-
|
|
289
|
+
// The path is a function of position, so a test parses under the same path on every run,
|
|
290
|
+
// which is what lets a parser pooled across specs reuse its program.
|
|
291
|
+
let unnamed = 0;
|
|
234
292
|
const before: [SourceSpec<any>, { text: string, sourcePath: string }][] = [];
|
|
235
293
|
for (const spec of specs) {
|
|
236
294
|
if (spec.before) {
|
|
237
|
-
const sourcePath = spec.path ||
|
|
295
|
+
const sourcePath = spec.path || `source-${unnamed++}.${spec.ext}`;
|
|
238
296
|
before.push([spec, {text: dedent(spec.before), sourcePath: sourcePath}]);
|
|
239
297
|
}
|
|
240
298
|
}
|