@openrewrite/rewrite 8.96.4 → 8.97.0
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/execution.d.ts +2 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +4 -0
- package/dist/execution.js.map +1 -1
- package/dist/heavyweight.d.ts +8 -0
- package/dist/heavyweight.d.ts.map +1 -0
- package/dist/heavyweight.js +45 -0
- package/dist/heavyweight.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/java/tree.d.ts +2 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js.map +1 -1
- package/dist/java/visitor.d.ts +5 -0
- package/dist/java/visitor.d.ts.map +1 -1
- package/dist/java/visitor.js +264 -217
- package/dist/java/visitor.js.map +1 -1
- package/dist/javascript/add-import.d.ts +2 -1
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +14 -2
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts.map +1 -1
- package/dist/javascript/amd.js +2 -1
- package/dist/javascript/amd.js.map +1 -1
- package/dist/javascript/cleanup/add-parse-int-radix.js +1 -1
- package/dist/javascript/cleanup/add-parse-int-radix.js.map +1 -1
- package/dist/javascript/dependency-workspace.d.ts +8 -2
- package/dist/javascript/dependency-workspace.d.ts.map +1 -1
- package/dist/javascript/dependency-workspace.js +36 -4
- package/dist/javascript/dependency-workspace.js.map +1 -1
- package/dist/javascript/format/format.d.ts +10 -0
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +10 -2
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.js +4 -0
- package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.js +4 -0
- package/dist/javascript/format/whitespace-reconciler.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +2 -0
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/parser-utils.d.ts.map +1 -1
- package/dist/javascript/parser-utils.js +9 -6
- package/dist/javascript/parser-utils.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +1 -0
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +39 -10
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +8 -0
- 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 +37 -1
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/visitor.d.ts.map +1 -1
- package/dist/javascript/visitor.js +153 -118
- package/dist/javascript/visitor.js.map +1 -1
- package/dist/recipe.d.ts +5 -0
- package/dist/recipe.d.ts.map +1 -1
- package/dist/recipe.js +5 -0
- package/dist/recipe.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +40 -2
- package/dist/run.js.map +1 -1
- package/dist/test/accumulator-contents.d.ts +15 -0
- package/dist/test/accumulator-contents.d.ts.map +1 -0
- package/dist/test/accumulator-contents.js +185 -0
- package/dist/test/accumulator-contents.js.map +1 -0
- package/dist/test/empty-sources.d.ts +14 -0
- package/dist/test/empty-sources.d.ts.map +1 -0
- package/dist/test/empty-sources.js +98 -0
- package/dist/test/empty-sources.js.map +1 -0
- package/dist/test/formatting-scope.d.ts +18 -0
- package/dist/test/formatting-scope.d.ts.map +1 -0
- package/dist/test/formatting-scope.js +244 -0
- package/dist/test/formatting-scope.js.map +1 -0
- package/dist/test/probes.d.ts +25 -0
- package/dist/test/probes.d.ts.map +1 -0
- package/dist/test/probes.js +189 -0
- package/dist/test/probes.js.map +1 -0
- package/dist/test/rewrite-test.d.ts +27 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +191 -21
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/test/tree-shapes.d.ts +15 -0
- package/dist/test/tree-shapes.d.ts.map +1 -0
- package/dist/test/tree-shapes.js +288 -0
- package/dist/test/tree-shapes.js.map +1 -0
- package/dist/test/unparsed-sources.d.ts +17 -0
- package/dist/test/unparsed-sources.d.ts.map +1 -0
- package/dist/test/unparsed-sources.js +113 -0
- package/dist/test/unparsed-sources.js.map +1 -0
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +2 -0
- package/dist/tree.js.map +1 -1
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +2 -0
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/execution.ts +6 -0
- package/src/heavyweight.ts +42 -0
- package/src/index.ts +1 -0
- package/src/java/tree.ts +2 -1
- package/src/java/visitor.ts +263 -216
- package/src/javascript/add-import.ts +11 -2
- package/src/javascript/amd.ts +4 -2
- package/src/javascript/cleanup/add-parse-int-radix.ts +2 -2
- package/src/javascript/dependency-workspace.ts +35 -3
- package/src/javascript/format/format.ts +18 -0
- package/src/javascript/format/tabs-and-indents-visitor.ts +4 -0
- package/src/javascript/format/whitespace-reconciler.ts +4 -0
- package/src/javascript/node-resolution-result.ts +3 -0
- package/src/javascript/parser-utils.ts +10 -6
- package/src/javascript/templating/engine.ts +41 -9
- package/src/javascript/templating/pattern.ts +10 -0
- package/src/javascript/templating/placeholder-replacement.ts +38 -1
- package/src/javascript/visitor.ts +166 -118
- package/src/recipe.ts +5 -0
- package/src/run.ts +30 -2
- package/src/test/accumulator-contents.ts +194 -0
- package/src/test/empty-sources.ts +99 -0
- package/src/test/formatting-scope.ts +231 -0
- package/src/test/probes.ts +156 -0
- package/src/test/rewrite-test.ts +177 -24
- package/src/test/tree-shapes.ts +332 -0
- package/src/test/unparsed-sources.ts +138 -0
- package/src/tree.ts +3 -0
- package/src/visitor.ts +3 -0
|
@@ -12,6 +12,7 @@ import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, i
|
|
|
12
12
|
import {create as produce, Draft} from "mutative";
|
|
13
13
|
import {autoFormat} from "./format";
|
|
14
14
|
import {getPrettierStyle} from "./format/prettier-format";
|
|
15
|
+
import ts from "typescript";
|
|
15
16
|
|
|
16
17
|
export type QuoteChar = "'" | '"';
|
|
17
18
|
|
|
@@ -562,7 +563,8 @@ function claimedName(v: unknown): string | undefined {
|
|
|
562
563
|
/**
|
|
563
564
|
* The parser lifts surrogate pairs out of `valueSource` into `unicodeEscapes` and, when it does,
|
|
564
565
|
* sets `value` to the quoted source (`parser.ts` `mapLiteral`), so neither field alone is the
|
|
565
|
-
* module name. Reuniting them and
|
|
566
|
+
* module name. Reuniting them and reading the quoted text as the language does yields it for
|
|
567
|
+
* either shape of literal.
|
|
566
568
|
*/
|
|
567
569
|
export function moduleNameOf(module: string | J.Literal): string {
|
|
568
570
|
if (typeof module === 'string') {
|
|
@@ -579,6 +581,13 @@ export function moduleNameOf(module: string | J.Literal): string {
|
|
|
579
581
|
cut = escape.valueSourceIndex;
|
|
580
582
|
}
|
|
581
583
|
restored += source.slice(cut);
|
|
584
|
+
// an escape such as `\u00dc` names the character it spells
|
|
585
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, restored);
|
|
586
|
+
const token = scanner.scan();
|
|
587
|
+
if ((token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) &&
|
|
588
|
+
scanner.getTokenEnd() === restored.length) {
|
|
589
|
+
return scanner.getTokenValue();
|
|
590
|
+
}
|
|
582
591
|
const quote = restored.charAt(0);
|
|
583
592
|
return (quote === "'" || quote === '"' || quote === '`') && restored.endsWith(quote) && restored.length > 1
|
|
584
593
|
? restored.slice(1, -1)
|
|
@@ -1583,7 +1592,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
|
1583
1592
|
value: this.module,
|
|
1584
1593
|
valueSource,
|
|
1585
1594
|
unicodeEscapes: this.moduleUnicodeEscapes,
|
|
1586
|
-
type:
|
|
1595
|
+
type: Type.Primitive.String
|
|
1587
1596
|
};
|
|
1588
1597
|
}
|
|
1589
1598
|
|
package/src/javascript/amd.ts
CHANGED
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
rightPadded,
|
|
26
26
|
space,
|
|
27
27
|
Statement,
|
|
28
|
-
TrailingComma
|
|
28
|
+
TrailingComma,
|
|
29
|
+
Type
|
|
29
30
|
} from "../java";
|
|
30
31
|
import {JS} from "./tree";
|
|
31
32
|
import {moduleNameOf} from "./add-import";
|
|
@@ -428,7 +429,8 @@ function dependencyLiteral(module: string, quote: string): J.Literal {
|
|
|
428
429
|
prefix: emptySpace,
|
|
429
430
|
markers: emptyMarkers,
|
|
430
431
|
value: module,
|
|
431
|
-
valueSource: `${quote}${module}${quote}
|
|
432
|
+
valueSource: `${quote}${module}${quote}`,
|
|
433
|
+
type: Type.Primitive.String
|
|
432
434
|
};
|
|
433
435
|
}
|
|
434
436
|
|
|
@@ -18,7 +18,7 @@ import {Recipe} from "../../recipe";
|
|
|
18
18
|
import {TreeVisitor} from "../../visitor";
|
|
19
19
|
import {ExecutionContext} from "../../execution";
|
|
20
20
|
import {JavaScriptVisitor} from "../visitor";
|
|
21
|
-
import {J, singleSpace} from "../../java";
|
|
21
|
+
import {J, singleSpace, Type} from "../../java";
|
|
22
22
|
import {emptyMarkers} from "../../markers";
|
|
23
23
|
import {randomId} from "../../uuid";
|
|
24
24
|
|
|
@@ -73,7 +73,7 @@ export class AddParseIntRadix extends Recipe {
|
|
|
73
73
|
value: 10,
|
|
74
74
|
valueSource: '10',
|
|
75
75
|
unicodeEscapes: undefined,
|
|
76
|
-
type:
|
|
76
|
+
type: Type.Primitive.Double
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
return {
|
|
@@ -229,6 +229,7 @@ export class DependencyWorkspace {
|
|
|
229
229
|
// Write package files
|
|
230
230
|
writePackageFiles(targetDir);
|
|
231
231
|
|
|
232
|
+
this.markUsed(cachedWorkspaceDir);
|
|
232
233
|
return targetDir;
|
|
233
234
|
} catch (symlinkError) {
|
|
234
235
|
// Symlink failed (e.g., cross-device, permissions) - fall through to npm install
|
|
@@ -257,6 +258,7 @@ export class DependencyWorkspace {
|
|
|
257
258
|
// Check cache
|
|
258
259
|
const cached = this.cache.get(hash);
|
|
259
260
|
if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, depsForValidation)) {
|
|
261
|
+
this.markUsed(cached);
|
|
260
262
|
return cached;
|
|
261
263
|
}
|
|
262
264
|
|
|
@@ -266,6 +268,7 @@ export class DependencyWorkspace {
|
|
|
266
268
|
// Check if valid workspace already exists on disk (cross-VM reuse)
|
|
267
269
|
if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, depsForValidation)) {
|
|
268
270
|
this.cache.set(hash, workspaceDir);
|
|
271
|
+
this.markUsed(workspaceDir);
|
|
269
272
|
return workspaceDir;
|
|
270
273
|
}
|
|
271
274
|
|
|
@@ -390,6 +393,19 @@ export class DependencyWorkspace {
|
|
|
390
393
|
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
391
394
|
}
|
|
392
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Marks a workspace used now, so that a cleanup ages it from its last use rather than from its install, and never
|
|
398
|
+
* deletes one a test run or a recipe has just used.
|
|
399
|
+
*/
|
|
400
|
+
private static markUsed(workspaceDir: string): void {
|
|
401
|
+
try {
|
|
402
|
+
const now = new Date();
|
|
403
|
+
fs.utimesSync(workspaceDir, now, now);
|
|
404
|
+
} catch {
|
|
405
|
+
// At worst, a cleanup deletes it once it is a day old, and it is installed again.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
393
409
|
/**
|
|
394
410
|
* Checks if a workspace is valid (has node_modules and matching package.json).
|
|
395
411
|
* Handles both real node_modules directories and symlinks to cached workspaces.
|
|
@@ -444,6 +460,15 @@ export class DependencyWorkspace {
|
|
|
444
460
|
return false;
|
|
445
461
|
}
|
|
446
462
|
}
|
|
463
|
+
|
|
464
|
+
// And installed: a workspace whose node_modules has lost a package, as a deletion cut short leaves
|
|
465
|
+
// one, would otherwise be reused without it, and nothing it declares would be type attributed. Its
|
|
466
|
+
// devDependencies are not asked for, which npm leaves out under NODE_ENV=production.
|
|
467
|
+
for (const name of Object.keys(packageJsonContent.dependencies ?? {})) {
|
|
468
|
+
if (!fs.existsSync(path.join(nodeModules, name, 'package.json'))) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
447
472
|
} catch (error) {
|
|
448
473
|
return false;
|
|
449
474
|
}
|
|
@@ -454,11 +479,12 @@ export class DependencyWorkspace {
|
|
|
454
479
|
|
|
455
480
|
/**
|
|
456
481
|
* Cleans up old workspace directories.
|
|
457
|
-
* Removes workspaces
|
|
482
|
+
* Removes workspaces unused for longer than the specified age, each moved out of its key's way before it is
|
|
483
|
+
* deleted, so that a deletion cut short leaves a temporary directory rather than part of a workspace.
|
|
458
484
|
* Also removes all temporary directories (*.tmp-*) regardless of age,
|
|
459
485
|
* as these indicate incomplete/crashed operations.
|
|
460
486
|
*
|
|
461
|
-
* @param maxAgeMs Maximum age in milliseconds (default: 24 hours)
|
|
487
|
+
* @param maxAgeMs Maximum age in milliseconds since a workspace was last used (default: 24 hours)
|
|
462
488
|
*/
|
|
463
489
|
static cleanupOldWorkspaces(maxAgeMs: number = 24 * 60 * 60 * 1000): void {
|
|
464
490
|
if (!fs.existsSync(this.WORKSPACE_BASE)) {
|
|
@@ -508,7 +534,12 @@ export class DependencyWorkspace {
|
|
|
508
534
|
const age = now - stats.mtimeMs;
|
|
509
535
|
|
|
510
536
|
if (age > maxAgeMs) {
|
|
511
|
-
|
|
537
|
+
// Out of its key's way in one rename, then deleted: a process looking the workspace up finds it
|
|
538
|
+
// whole or not at all. A deletion cut short, as when the RPC server is killed while it shuts
|
|
539
|
+
// down, then leaves a temporary directory of a process that has gone, which the next cleanup
|
|
540
|
+
// removes, rather than a workspace that has lost part of its node_modules and still looks valid.
|
|
541
|
+
const deleting = `${workspaceDir}.tmp-${process.pid}-${Date.now()}-deleting`;
|
|
542
|
+
fs.renameSync(workspaceDir, deleting);
|
|
512
543
|
// Remove from cache
|
|
513
544
|
for (const [hash, dir] of this.cache.entries()) {
|
|
514
545
|
if (dir === workspaceDir) {
|
|
@@ -516,6 +547,7 @@ export class DependencyWorkspace {
|
|
|
516
547
|
break;
|
|
517
548
|
}
|
|
518
549
|
}
|
|
550
|
+
fs.rmSync(deleting, {recursive: true, force: true});
|
|
519
551
|
}
|
|
520
552
|
} catch (error) {
|
|
521
553
|
// Ignore errors, workspace might be in use
|
|
@@ -26,6 +26,7 @@ import {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
|
|
|
26
26
|
import {NormalizeWhitespaceVisitor} from "./normalize-whitespace-visitor";
|
|
27
27
|
import {MinimumViableSpacingVisitor} from "./minimum-viable-spacing-visitor";
|
|
28
28
|
import {applyPrettierFormatting, getPrettierStyle} from "./prettier-format";
|
|
29
|
+
import {AsyncLocalStorage} from "node:async_hooks";
|
|
29
30
|
|
|
30
31
|
export {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
|
|
31
32
|
export {NormalizeWhitespaceVisitor} from "./normalize-whitespace-visitor";
|
|
@@ -56,6 +57,22 @@ export const autoIndent = async <J2 extends J, P>(j: J2, p: P, parent?: Cursor):
|
|
|
56
57
|
return await new TabsAndIndentsVisitor(style).visit(j, p, parent) as J2;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Hears about each request to auto-format a tree made while it listens, so that a test harness can check that a
|
|
62
|
+
* recipe formats only what it changed.
|
|
63
|
+
*/
|
|
64
|
+
export interface FormattingListener {
|
|
65
|
+
/** AutoformatVisitor calls this before it formats `tree`, stopping after `stopAfter` when it is given. */
|
|
66
|
+
onFormat(tree: Tree, stopAfter: Tree | undefined, cursor: Cursor | undefined): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const formattingListener = new AsyncLocalStorage<FormattingListener>();
|
|
70
|
+
|
|
71
|
+
/** Runs `run`, telling `listener` about the formatting it requests. */
|
|
72
|
+
export function listenToFormatting<T>(listener: FormattingListener, run: () => Promise<T>): Promise<T> {
|
|
73
|
+
return formattingListener.run(listener, run);
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
/**
|
|
60
77
|
* Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
|
|
61
78
|
*
|
|
@@ -77,6 +94,7 @@ export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
async visit<R extends J>(tree: Tree, p: P, cursor?: Cursor): Promise<R | undefined> {
|
|
97
|
+
formattingListener.getStore()?.onFormat(tree, this.stopAfter, cursor);
|
|
80
98
|
const prettierStyle = getPrettierStyle(tree, cursor, this.styles);
|
|
81
99
|
if (prettierStyle) {
|
|
82
100
|
const formatted = await applyPrettierFormatting(tree as R, prettierStyle, p, cursor, this.stopAfter);
|
|
@@ -408,6 +408,10 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
408
408
|
return literal;
|
|
409
409
|
}
|
|
410
410
|
return produce(literal, draft => {
|
|
411
|
+
// JSX text is its own value, as the parser makes it
|
|
412
|
+
if (draft.value === draft.valueSource) {
|
|
413
|
+
draft.value = normalizedValueSource;
|
|
414
|
+
}
|
|
411
415
|
draft.valueSource = normalizedValueSource;
|
|
412
416
|
});
|
|
413
417
|
}
|
|
@@ -416,6 +416,10 @@ export class WhitespaceReconciler {
|
|
|
416
416
|
original: J.Identifier | J.Literal,
|
|
417
417
|
formatted: J.Identifier | J.Literal
|
|
418
418
|
): J.Identifier | J.Literal {
|
|
419
|
+
// A literal's type follows from how it is written, so an identifier's is not the literal's
|
|
420
|
+
if (isLiteral(formatted)) {
|
|
421
|
+
return formatted;
|
|
422
|
+
}
|
|
419
423
|
const result: Record<string, unknown> = { ...formatted };
|
|
420
424
|
|
|
421
425
|
// Preserve type attribution - both Identifier and Literal have `type`
|
|
@@ -19,6 +19,7 @@ import {asRef} from "../reference";
|
|
|
19
19
|
import {RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "../rpc";
|
|
20
20
|
import {castDraft} from "mutative";
|
|
21
21
|
import {updateIfChanged} from "../util";
|
|
22
|
+
import {registerHeavyweight} from "../heavyweight";
|
|
22
23
|
import * as semver from "semver";
|
|
23
24
|
import * as fsp from "fs/promises";
|
|
24
25
|
import * as path from "path";
|
|
@@ -259,6 +260,8 @@ export interface NodeResolutionResult extends Marker {
|
|
|
259
260
|
readonly npmrcConfigs?: Npmrc[];
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
registerHeavyweight(NodeResolutionResultKind);
|
|
264
|
+
|
|
262
265
|
/**
|
|
263
266
|
* Creates a NodeResolutionResult marker from a package.json file.
|
|
264
267
|
* Should be called during parsing to attach to JS.CompilationUnit.
|
|
@@ -17,7 +17,8 @@ import * as ts from "typescript";
|
|
|
17
17
|
import {Expression, J, Statement} from "../java";
|
|
18
18
|
import {JS} from "./tree";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Built on first use, since ./tree imports the visitor, which imports this module
|
|
21
|
+
const statementKinds = (): string[] => [
|
|
21
22
|
J.Kind.Assert,
|
|
22
23
|
J.Kind.Assignment,
|
|
23
24
|
J.Kind.AssignmentOperation,
|
|
@@ -78,9 +79,9 @@ const is_statements = new Set<string>([
|
|
|
78
79
|
JS.Kind.WithStatement,
|
|
79
80
|
JS.Kind.ExpressionStatement,
|
|
80
81
|
JS.Kind.StatementExpression
|
|
81
|
-
]
|
|
82
|
+
];
|
|
82
83
|
|
|
83
|
-
const
|
|
84
|
+
const expressionKinds = (): string[] => [
|
|
84
85
|
J.Kind.AnnotatedType,
|
|
85
86
|
J.Kind.Annotation,
|
|
86
87
|
J.Kind.ArrayAccess,
|
|
@@ -157,14 +158,17 @@ const is_expressions = new Set<string>([
|
|
|
157
158
|
JS.Kind.Void,
|
|
158
159
|
JS.Kind.ExpressionStatement,
|
|
159
160
|
JS.Kind.StatementExpression
|
|
160
|
-
]
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
let is_statements: Set<string> | undefined;
|
|
164
|
+
let is_expressions: Set<string> | undefined;
|
|
161
165
|
|
|
162
166
|
export function isStatement(statement: J): statement is Statement {
|
|
163
|
-
return is_statements.has(statement.kind);
|
|
167
|
+
return (is_statements ??= new Set(statementKinds())).has(statement.kind);
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
export function isExpression(expression: J): expression is Expression {
|
|
167
|
-
return is_expressions.has(expression.kind);
|
|
171
|
+
return (is_expressions ??= new Set(expressionKinds())).has(expression.kind);
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
export function getNextSibling(node: ts.Node): ts.Node | undefined {
|
|
@@ -481,9 +481,15 @@ export class TemplateEngine {
|
|
|
481
481
|
* Converts a Type instance to a TypeScript type string.
|
|
482
482
|
*
|
|
483
483
|
* @param type The Type instance
|
|
484
|
+
* @param enclosing The types that `type` is a type argument or element type of
|
|
484
485
|
* @returns A TypeScript type string
|
|
485
486
|
*/
|
|
486
|
-
private static typeToString(type: Type): string {
|
|
487
|
+
private static typeToString(type: Type, enclosing: Type[] = []): string {
|
|
488
|
+
// A recursive type, such as `type A = Array<A>`, is any where it holds itself
|
|
489
|
+
if (enclosing.includes(type)) {
|
|
490
|
+
return 'any';
|
|
491
|
+
}
|
|
492
|
+
|
|
487
493
|
// Handle Type.Class and Type.ShallowClass - return their fully qualified names
|
|
488
494
|
if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
|
|
489
495
|
const classType = type as Type.Class;
|
|
@@ -518,10 +524,18 @@ export class TemplateEngine {
|
|
|
518
524
|
// Handle Type.Array - render component type plus []
|
|
519
525
|
if (type.kind === Type.Kind.Array) {
|
|
520
526
|
const arrayType = type as Type.Array;
|
|
521
|
-
const componentTypeString = this.typeToString(arrayType.elemType);
|
|
527
|
+
const componentTypeString = this.typeToString(arrayType.elemType, [...enclosing, type]);
|
|
522
528
|
return `${componentTypeString}[]`;
|
|
523
529
|
}
|
|
524
530
|
|
|
531
|
+
// An array, a map or any other generic type, with its type arguments
|
|
532
|
+
if (type.kind === Type.Kind.Parameterized) {
|
|
533
|
+
const parameterized = type as Type.Parameterized;
|
|
534
|
+
const base = this.typeToString(parameterized.type);
|
|
535
|
+
return base === 'any' || parameterized.typeParameters.length === 0 ? base :
|
|
536
|
+
`${base}<${parameterized.typeParameters.map(t => this.typeToString(t, [...enclosing, type])).join(', ')}>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
525
539
|
// For other types, return 'any' as a fallback
|
|
526
540
|
// TODO: Implement proper Type to string conversion for other Type.Kind values
|
|
527
541
|
return 'any';
|
|
@@ -756,17 +770,22 @@ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
|
|
|
756
770
|
}
|
|
757
771
|
|
|
758
772
|
// FIXME: This is a heuristic to determine if the parent expects a statement child
|
|
759
|
-
function expectsStatement(parentTree: J): boolean {
|
|
773
|
+
function expectsStatement(parentTree: J, childId: string): boolean {
|
|
774
|
+
if (parentTree.kind === J.Kind.Case) {
|
|
775
|
+
// the labels of a case are expressions
|
|
776
|
+
return !(parentTree as J.Case).caseLabels.elements.some(label => label.element.id === childId);
|
|
777
|
+
}
|
|
760
778
|
return parentTree.kind === J.Kind.Block ||
|
|
761
|
-
parentTree.kind === J.Kind.Case ||
|
|
762
779
|
parentTree.kind === J.Kind.DoWhileLoop ||
|
|
763
780
|
parentTree.kind === J.Kind.ForEachLoop ||
|
|
764
781
|
parentTree.kind === J.Kind.ForLoop ||
|
|
765
782
|
parentTree.kind === J.Kind.If ||
|
|
766
783
|
parentTree.kind === J.Kind.IfElse ||
|
|
784
|
+
parentTree.kind === J.Kind.Label ||
|
|
767
785
|
parentTree.kind === J.Kind.WhileLoop ||
|
|
768
786
|
parentTree.kind === JS.Kind.CompilationUnit ||
|
|
769
|
-
parentTree.kind === JS.Kind.ForInLoop
|
|
787
|
+
parentTree.kind === JS.Kind.ForInLoop ||
|
|
788
|
+
parentTree.kind === JS.Kind.WithStatement;
|
|
770
789
|
}
|
|
771
790
|
|
|
772
791
|
/** Whether the code opens with `{`, which is a block where a statement goes and an object literal elsewhere. */
|
|
@@ -782,7 +801,7 @@ export function replacedByObjectLiteral(tree: J, cursor: Cursor): boolean {
|
|
|
782
801
|
return true;
|
|
783
802
|
}
|
|
784
803
|
const parentTree = cursor.parentTree()?.value;
|
|
785
|
-
return parentTree !== undefined && !expectsStatement(parentTree);
|
|
804
|
+
return parentTree !== undefined && !expectsStatement(parentTree, tree.id);
|
|
786
805
|
}
|
|
787
806
|
|
|
788
807
|
/**
|
|
@@ -913,9 +932,21 @@ export class TemplateApplier {
|
|
|
913
932
|
private wrapTree(originalTree: J, resultToUse: J) {
|
|
914
933
|
const parentTree = this.cursor?.parentTree()?.value;
|
|
915
934
|
|
|
935
|
+
// A `for` head holds each expression of its init and update in an expression statement, as the parser does
|
|
936
|
+
if (parentTree?.kind === J.Kind.ForLoopControl &&
|
|
937
|
+
(parentTree as J.ForLoop.Control).condition?.element.id !== originalTree.id) {
|
|
938
|
+
return isExpression(resultToUse) && resultToUse.kind !== JS.Kind.ExpressionStatement ? {
|
|
939
|
+
kind: JS.Kind.ExpressionStatement,
|
|
940
|
+
id: randomId(),
|
|
941
|
+
prefix: resultToUse.prefix,
|
|
942
|
+
markers: wrapperMarkers(resultToUse),
|
|
943
|
+
expression: {...resultToUse, prefix: emptySpace}
|
|
944
|
+
} as JS.ExpressionStatement : resultToUse;
|
|
945
|
+
}
|
|
946
|
+
|
|
916
947
|
// Only apply wrapping logic if we have parent context
|
|
917
948
|
if (parentTree) {
|
|
918
|
-
const parentExpectsStatement = expectsStatement(parentTree);
|
|
949
|
+
const parentExpectsStatement = expectsStatement(parentTree, originalTree.id);
|
|
919
950
|
const originalIsStatement = isStatement(originalTree);
|
|
920
951
|
|
|
921
952
|
const resultIsStatement = isStatement(resultToUse);
|
|
@@ -935,8 +966,9 @@ export class TemplateApplier {
|
|
|
935
966
|
expression: { ...expression, prefix: emptySpace }
|
|
936
967
|
} as JS.ExpressionStatement;
|
|
937
968
|
}
|
|
938
|
-
} else if (!parentExpectsStatement) {
|
|
939
|
-
// Expression context: wrap in StatementExpression if result is statement-only
|
|
969
|
+
} else if (!parentExpectsStatement && isExpression(originalTree)) {
|
|
970
|
+
// Expression context: wrap in StatementExpression if result is statement-only. A slot that held
|
|
971
|
+
// a statement-only node, such as an arrow function's block body, takes one unwrapped.
|
|
940
972
|
if (resultIsStatement && !resultIsExpression) {
|
|
941
973
|
const stmt = resultToUse as Statement;
|
|
942
974
|
resultToUse = {
|
|
@@ -756,6 +756,16 @@ class Matcher {
|
|
|
756
756
|
opensWithBrace(this.pattern.templateParts) && this.ast.kind !== J.Kind.Block);
|
|
757
757
|
}
|
|
758
758
|
|
|
759
|
+
// A visit reaches what parentheses hold right after them, so seeing through them here would match it twice
|
|
760
|
+
if ((this.ast.kind === J.Kind.Parentheses || this.ast.kind === J.Kind.ControlParentheses) &&
|
|
761
|
+
this.patternAst.kind !== this.ast.kind) {
|
|
762
|
+
if (this.debugOptions.enabled) {
|
|
763
|
+
this.setExplanation('kind-mismatch', 'An expression without parentheses',
|
|
764
|
+
`Parentheses (${this.ast.kind.split('.').pop()})`,
|
|
765
|
+
'The expression the parentheses hold is matched on its own');
|
|
766
|
+
}
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
759
769
|
return this.matchNode(this.patternAst, this.ast);
|
|
760
770
|
}
|
|
761
771
|
|
|
@@ -41,6 +41,25 @@ export class SubstitutedValue implements Marker {
|
|
|
41
41
|
/** Chooses a substituted value's prefix, and says whether it is the source's own. */
|
|
42
42
|
type MergePrefix = (sourcePrefix: J.Space, templatePrefix: J.Space) => [J.Space, boolean];
|
|
43
43
|
|
|
44
|
+
/** Whether `value` is a property, method or spread of an object literal. */
|
|
45
|
+
function isObjectMember(value: unknown): value is J {
|
|
46
|
+
switch (isTree(value) && value.kind) {
|
|
47
|
+
case JS.Kind.PropertyAssignment:
|
|
48
|
+
case JS.Kind.Spread:
|
|
49
|
+
case J.Kind.MethodDeclaration:
|
|
50
|
+
case JS.Kind.ComputedPropertyMethodDeclaration:
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `padded` less the trailing comma it brought from the end of the list it was taken from. */
|
|
57
|
+
function withoutTrailingComma(padded: J.RightPadded<J>): J.RightPadded<J> {
|
|
58
|
+
const markers = padded.markers?.markers;
|
|
59
|
+
return markers?.some(m => m.kind === J.Markers.TrailingComma) ?
|
|
60
|
+
{...padded, markers: {...padded.markers, markers: markers.filter(m => m.kind !== J.Markers.TrailingComma)}} : padded;
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
/**
|
|
45
64
|
* Visitor that replaces placeholder nodes with actual parameter values.
|
|
46
65
|
*/
|
|
@@ -319,6 +338,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
319
338
|
const param = this.substitutions.get(placeholderText);
|
|
320
339
|
if (param) {
|
|
321
340
|
let arrayToExpand: J[] | J.RightPadded<J>[] | undefined = undefined;
|
|
341
|
+
const member = element.kind === JS.Kind.PropertyAssignment ? this.objectMember(param.value) : undefined;
|
|
322
342
|
|
|
323
343
|
// Check if it's a J.Container
|
|
324
344
|
const isContainer = param.value && typeof param.value === 'object' &&
|
|
@@ -331,6 +351,10 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
331
351
|
else if (Array.isArray(param.value)) {
|
|
332
352
|
arrayToExpand = param.value as J[];
|
|
333
353
|
}
|
|
354
|
+
// One member of an object literal fills the slot of a member, not the name of a property
|
|
355
|
+
else if (member) {
|
|
356
|
+
arrayToExpand = [member] as J[] | J.RightPadded<J>[];
|
|
357
|
+
}
|
|
334
358
|
// Check if it's a CaptureValue
|
|
335
359
|
else if (param.value instanceof CaptureValue) {
|
|
336
360
|
const resolved = param.value.resolve(this.values);
|
|
@@ -435,7 +459,20 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
|
|
|
435
459
|
}
|
|
436
460
|
}
|
|
437
461
|
|
|
438
|
-
|
|
462
|
+
// Only the last element prints the trailing comma that a given or captured padding brings along
|
|
463
|
+
return newElements.map((padded, i) => i === newElements.length - 1 ? padded : withoutTrailingComma(padded));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The member of an object literal, padded or not, that a parameter is or is bound to, if it is one. */
|
|
467
|
+
private objectMember(value: any): J | J.RightPadded<J> | undefined {
|
|
468
|
+
if (value instanceof CaptureValue) {
|
|
469
|
+
value = value.resolve(this.values);
|
|
470
|
+
} else if (value instanceof TemplateParamImpl) {
|
|
471
|
+
value = this.values.get(value.name);
|
|
472
|
+
} else if (value instanceof CaptureImpl || (value && typeof value === 'object' && value[CAPTURE_NAME_SYMBOL])) {
|
|
473
|
+
value = this.values.get(value[CAPTURE_NAME_SYMBOL] || value.name);
|
|
474
|
+
}
|
|
475
|
+
return isObjectMember(value?.kind === J.Kind.RightPadded ? value.element : value) ? value : undefined;
|
|
439
476
|
}
|
|
440
477
|
|
|
441
478
|
/** As `visit` on a placeholder, with `merge` choosing the replacement's prefix. */
|