@openrewrite/rewrite 8.96.4 → 8.96.5
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/visitor.d.ts.map +1 -1
- package/dist/java/visitor.js +16 -2
- 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/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.map +1 -1
- package/dist/javascript/templating/engine.js +11 -6
- 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.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +15 -0
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/visitor.d.ts.map +1 -1
- package/dist/javascript/visitor.js +21 -0
- 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/visitor.ts +16 -2
- 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/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 +11 -6
- package/src/javascript/templating/pattern.ts +10 -0
- package/src/javascript/templating/placeholder-replacement.ts +16 -0
- package/src/javascript/visitor.ts +30 -0
- 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
package/src/test/rewrite-test.ts
CHANGED
|
@@ -19,15 +19,24 @@ import {noopVisitor, TreeVisitor} from "../visitor";
|
|
|
19
19
|
import {Parser} from "../parser";
|
|
20
20
|
import {MarkerPrinter, PrintOutputCapture, TreePrinters} from "../print";
|
|
21
21
|
import {Cursor, isTree, rootCursor, SourceFile} from "../tree";
|
|
22
|
-
import {Result, scheduleRun} from "../run";
|
|
22
|
+
import {ACCUMULATOR_LISTENER, Result, scheduleRun} from "../run";
|
|
23
23
|
import {mapAsync, trimIndent} from "../util";
|
|
24
24
|
import {isParseError, ParseErrorKind} from "../parse-error";
|
|
25
25
|
import {Marker, Markers, MarkersKind, ParseExceptionResult} from "../markers";
|
|
26
26
|
import {isNamedStyles} from "../style";
|
|
27
27
|
import {JavaScriptVisitor} from "../javascript";
|
|
28
|
+
import {listenToFormatting} from "../javascript/format";
|
|
28
29
|
import {attributionHint, dumpTypesIfRequested} from "../javascript/type-report";
|
|
29
30
|
import {J, Type} from "../java";
|
|
30
31
|
import {DATA_TABLE_STORE, InMemoryDataTableStore} from "../data-table";
|
|
32
|
+
import * as path from "path";
|
|
33
|
+
import {AccumulatorContents} from "./accumulator-contents";
|
|
34
|
+
import {EmptySources} from "./empty-sources";
|
|
35
|
+
import {UnparsedSources} from "./unparsed-sources";
|
|
36
|
+
import {TreeShapes} from "./tree-shapes";
|
|
37
|
+
import {FormattingScope} from "./formatting-scope";
|
|
38
|
+
import {RpcRecipe} from "../rpc/recipe";
|
|
39
|
+
import {DelegatingRecipe} from "../rpc/java-recipe";
|
|
31
40
|
|
|
32
41
|
export interface SourceSpec<T extends SourceFile> {
|
|
33
42
|
kind: string,
|
|
@@ -45,6 +54,11 @@ interface RecipeSpecConfiguration {
|
|
|
45
54
|
checkParsePrintIdempotence: boolean;
|
|
46
55
|
allowEmptyDiff: boolean;
|
|
47
56
|
checkReportingMarkersOnly: boolean;
|
|
57
|
+
checkExecutionContextUnchanged: boolean;
|
|
58
|
+
checkNoEditsInScanningPhase: boolean;
|
|
59
|
+
checkLightweightAccumulators: boolean;
|
|
60
|
+
checkParserShapedTrees: boolean;
|
|
61
|
+
checkMinimalFormatting: boolean;
|
|
48
62
|
cycles?: number;
|
|
49
63
|
expectedCyclesThatMakeChanges?: number;
|
|
50
64
|
recipe: Recipe;
|
|
@@ -62,6 +76,33 @@ export class RecipeSpec {
|
|
|
62
76
|
*/
|
|
63
77
|
checkReportingMarkersOnly: boolean = true
|
|
64
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Fails a run whose recipe puts a message on the execution context, which every recipe and source file in a run
|
|
81
|
+
* shares.
|
|
82
|
+
*/
|
|
83
|
+
checkExecutionContextUnchanged: boolean = true
|
|
84
|
+
|
|
85
|
+
/** Fails a run whose ScanningRecipe's scanner edits a tree, which the scanning phase discards. */
|
|
86
|
+
checkNoEditsInScanningPhase: boolean = true
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Fails a run whose ScanningRecipe's accumulator keeps a tree, cursor, visitor, execution context, dependency model
|
|
90
|
+
* or other type given to registerHeavyweight, which would stay in memory all cycle.
|
|
91
|
+
*/
|
|
92
|
+
checkLightweightAccumulators: boolean = true
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fails a run whose output holds a node that the source's parser builds differently from the text it prints, such
|
|
96
|
+
* as a J.Identifier where the parser makes a J.Literal of `null`.
|
|
97
|
+
*/
|
|
98
|
+
checkParserShapedTrees: boolean = true
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fails a run whose recipe auto-formats a whole source file or class that existed before the run, instead of only
|
|
102
|
+
* what it changed.
|
|
103
|
+
*/
|
|
104
|
+
checkMinimalFormatting: boolean = true
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* The cycles to run, by default one more than {@link expectedCyclesThatMakeChanges} and at least two. The
|
|
67
108
|
* cycles after the expected ones, and a run on a fresh parse of the output, fail the test if they change
|
|
@@ -114,6 +155,11 @@ export class RecipeSpec {
|
|
|
114
155
|
checkParsePrintIdempotence: this.checkParsePrintIdempotence,
|
|
115
156
|
allowEmptyDiff: this.allowEmptyDiff,
|
|
116
157
|
checkReportingMarkersOnly: this.checkReportingMarkersOnly,
|
|
158
|
+
checkExecutionContextUnchanged: this.checkExecutionContextUnchanged,
|
|
159
|
+
checkNoEditsInScanningPhase: this.checkNoEditsInScanningPhase,
|
|
160
|
+
checkLightweightAccumulators: this.checkLightweightAccumulators,
|
|
161
|
+
checkParserShapedTrees: this.checkParserShapedTrees,
|
|
162
|
+
checkMinimalFormatting: this.checkMinimalFormatting,
|
|
117
163
|
cycles: this.cycles,
|
|
118
164
|
expectedCyclesThatMakeChanges: this.expectedCyclesThatMakeChanges,
|
|
119
165
|
recipe: this.recipe,
|
|
@@ -217,21 +263,43 @@ export class RecipeSpec {
|
|
|
217
263
|
}
|
|
218
264
|
|
|
219
265
|
this.recipeExecutionContext.messages[DATA_TABLE_STORE] = new InMemoryDataTableStore();
|
|
266
|
+
this.recipeExecutionContext.messages[ExecutionContext.SCANNING_MUTATION_VALIDATION] =
|
|
267
|
+
this.checkNoEditsInScanningPhase;
|
|
268
|
+
const accumulatorContents = this.checkLightweightAccumulators ? new AccumulatorContents() : undefined;
|
|
269
|
+
this.recipeExecutionContext.messages[ACCUMULATOR_LISTENER] = accumulatorContents?.listener;
|
|
270
|
+
const contextWrites = this.checkExecutionContextUnchanged ? new ExecutionContextWrites() : undefined;
|
|
271
|
+
const runContext = (ctx: ExecutionContext) => contextWrites?.watch(ctx) ?? ctx;
|
|
220
272
|
const expectedCycles = this.expectedCyclesThatMakeChanges ?? 1;
|
|
221
273
|
const cycles = this.cycles ?? Math.max(2, expectedCycles + 1);
|
|
222
|
-
const
|
|
223
|
-
|
|
274
|
+
const formattingScope = this.checkMinimalFormatting ? new FormattingScope(this.recipe) : undefined;
|
|
275
|
+
const run = () => this.runCycles(allParsed.map(([_, sourceFile]) => sourceFile), cycles, expectedCycles,
|
|
276
|
+
runContext);
|
|
277
|
+
const {changeset, unsettled} = formattingScope ? await listenToFormatting(formattingScope, run) : await run();
|
|
278
|
+
contextWrites?.assertNone(this.recipe);
|
|
224
279
|
|
|
225
280
|
this.expectNoUnexpectedGeneratedFiles(flattenedSpecs, changeset);
|
|
226
281
|
this.checkReportingMarkersOnly && await this.expectNoAddedMarkers(flattenedSpecs, allParsed, changeset);
|
|
282
|
+
accumulatorContents?.assertNoneFound();
|
|
227
283
|
await this.expectResultsToMatchAfter(flattenedSpecs, changeset, allParsed);
|
|
228
284
|
await this.expectGeneratedFiles(flattenedSpecs, changeset);
|
|
229
285
|
if (unsettled) {
|
|
230
286
|
await this.expectNoFurtherChanges(unsettled.changes,
|
|
231
287
|
`Expected recipe to complete in ${pluralCycles(expectedCycles)}, but cycle ${unsettled.cycle} changed`);
|
|
232
288
|
}
|
|
233
|
-
|
|
289
|
+
await formattingScope?.assertFormattedOnlyWhatChanged(changeset);
|
|
290
|
+
const changed = changeset.some(r => r.before !== r.after);
|
|
291
|
+
const freshParse = changed && (cycles > expectedCycles || this.checkParserShapedTrees) ?
|
|
292
|
+
await this.freshParse(flattenedSpecs, allParsed, changeset) : new Map<SourceFile, SourceFile>();
|
|
293
|
+
this.checkParserShapedTrees && await new TreeShapes(this.recipe).assertParserShaped(changeset, freshParse);
|
|
294
|
+
cycles > expectedCycles && await this.expectStableOnFreshParse(changeset, freshParse, runContext);
|
|
295
|
+
contextWrites?.assertNone(this.recipe);
|
|
234
296
|
await this.expectDataTables();
|
|
297
|
+
if (changeset.some(r => r.before && r.after !== r.before) && await isProbed(this.recipe)) {
|
|
298
|
+
await new EmptySources(this.recipe, changeset)
|
|
299
|
+
.assertNoneThrow(allParsed, this.executionContext, this.recipeExecutionContext, cycles);
|
|
300
|
+
await new UnparsedSources(this.recipe, changeset)
|
|
301
|
+
.assertNoneThrow(allParsed.map(([_, sourceFile]) => sourceFile), this.recipeExecutionContext, cycles);
|
|
302
|
+
}
|
|
235
303
|
}
|
|
236
304
|
|
|
237
305
|
private async expectDataTables() {
|
|
@@ -247,7 +315,8 @@ export class RecipeSpec {
|
|
|
247
315
|
}
|
|
248
316
|
}
|
|
249
317
|
|
|
250
|
-
private async runCycles(sources: SourceFile[], cycles: number, expectedCycles: number
|
|
318
|
+
private async runCycles(sources: SourceFile[], cycles: number, expectedCycles: number,
|
|
319
|
+
runContext: (ctx: ExecutionContext) => ExecutionContext):
|
|
251
320
|
Promise<{ changeset: Result[], unsettled?: { cycle: number, changes: Result[] } }> {
|
|
252
321
|
const lineages: { origin?: SourceFile, latest?: SourceFile }[] = sources.map(s => ({origin: s, latest: s}));
|
|
253
322
|
const changesetOf = () => lineages.map(({origin, latest}) => new Result(origin, latest));
|
|
@@ -255,7 +324,7 @@ export class RecipeSpec {
|
|
|
255
324
|
let cyclesThatMadeChanges = 0;
|
|
256
325
|
for (let cycle = 1; cycle <= cycles; cycle++) {
|
|
257
326
|
const live = lineages.filter(l => l.latest).map(l => l.latest!);
|
|
258
|
-
const ctx = cycle === 1 ? this.recipeExecutionContext : this.laterRunContext();
|
|
327
|
+
const ctx = runContext(cycle === 1 ? this.recipeExecutionContext : this.laterRunContext());
|
|
259
328
|
const changes = (await scheduleRun(this.recipe, live, ctx)).changeset.filter(r => r.before !== r.after);
|
|
260
329
|
if (changes.length === 0) {
|
|
261
330
|
break;
|
|
@@ -319,14 +388,11 @@ export class RecipeSpec {
|
|
|
319
388
|
}
|
|
320
389
|
|
|
321
390
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
391
|
+
* A fresh parse, by the parser of its spec, of the text each source the recipe left prints, parsing the sources
|
|
392
|
+
* of one kind together so that they can refer to one another. A parser that throws leaves its sources out.
|
|
324
393
|
*/
|
|
325
|
-
private async
|
|
326
|
-
|
|
327
|
-
if (!changeset.some(r => r.before !== r.after)) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
394
|
+
private async freshParse(specs: SourceSpec<any>[], parsed: [SourceSpec<any>, SourceFile][],
|
|
395
|
+
changeset: Result[]): Promise<Map<SourceFile, SourceFile>> {
|
|
330
396
|
const finals = changeset.filter(r => r.after).map(({before, after}): [SourceSpec<any> | undefined, SourceFile] => [
|
|
331
397
|
before ? parsed.find(([_, sourceFile]) => sourceFile === before)?.[0] :
|
|
332
398
|
specs.find(s => s.path === after!.sourcePath),
|
|
@@ -344,22 +410,39 @@ export class RecipeSpec {
|
|
|
344
410
|
let i = 0;
|
|
345
411
|
try {
|
|
346
412
|
for await (const reparsed of group[0][0]!.parser(this.executionContext).parse(...inputs)) {
|
|
347
|
-
|
|
348
|
-
// What the source file carries, like its styles, comes from outside its text
|
|
349
|
-
reparsedByFinal.set(group[i][1], {...reparsed, markers: group[i][1].markers});
|
|
350
|
-
}
|
|
351
|
-
i++;
|
|
413
|
+
reparsedByFinal.set(group[i++][1], reparsed);
|
|
352
414
|
}
|
|
353
415
|
} catch {
|
|
416
|
+
// the sources it didn't get to are left out
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return reparsedByFinal;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Code that the next run reads is parsed from the printed output, not the LST this run produced, and a
|
|
424
|
+
* recipe that recognizes its own output only by the shape of that LST changes it again.
|
|
425
|
+
*/
|
|
426
|
+
private async expectStableOnFreshParse(changeset: Result[], freshParse: Map<SourceFile, SourceFile>,
|
|
427
|
+
runContext: (ctx: ExecutionContext) => ExecutionContext) {
|
|
428
|
+
let reparsedAny = false;
|
|
429
|
+
const sources: SourceFile[] = [];
|
|
430
|
+
for (const after of changeset.filter(r => r.after).map(r => r.after!)) {
|
|
431
|
+
const reparsed = freshParse.get(after);
|
|
432
|
+
if (reparsed && !isParseError(reparsed) && await printSanitized(reparsed) === await printSanitized(after)) {
|
|
433
|
+
// What the source file carries, like its styles, comes from outside its text
|
|
434
|
+
sources.push({...reparsed, markers: after.markers});
|
|
435
|
+
reparsedAny = true;
|
|
436
|
+
} else {
|
|
354
437
|
// files that do not parse again keep the LST the recipe produced
|
|
438
|
+
sources.push(after);
|
|
355
439
|
}
|
|
356
440
|
}
|
|
357
|
-
if (
|
|
441
|
+
if (!reparsedAny) {
|
|
358
442
|
return;
|
|
359
443
|
}
|
|
360
444
|
|
|
361
|
-
const
|
|
362
|
-
const changes = (await scheduleRun(this.recipe, sources, this.laterRunContext())).changeset
|
|
445
|
+
const changes = (await scheduleRun(this.recipe, sources, runContext(this.laterRunContext()))).changeset
|
|
363
446
|
.filter(r => r.before !== r.after);
|
|
364
447
|
await this.expectNoFurtherChanges(changes, "Running the recipe on a fresh parse of its output changed", true);
|
|
365
448
|
}
|
|
@@ -501,7 +584,7 @@ export class RecipeSpec {
|
|
|
501
584
|
|
|
502
585
|
private async expectGeneratedFiles(specs: SourceSpec<any>[], changeset: Result[]) {
|
|
503
586
|
for (const spec of specs) {
|
|
504
|
-
if (spec.before) {
|
|
587
|
+
if (spec.before !== null) {
|
|
505
588
|
continue;
|
|
506
589
|
}
|
|
507
590
|
if (!spec.after) {
|
|
@@ -513,7 +596,7 @@ export class RecipeSpec {
|
|
|
513
596
|
}
|
|
514
597
|
|
|
515
598
|
private expectNoUnexpectedGeneratedFiles(specs: SourceSpec<any>[], changeset: Result[]) {
|
|
516
|
-
const expectedPaths = new Set(specs.filter(spec =>
|
|
599
|
+
const expectedPaths = new Set(specs.filter(spec => spec.before === null).map(spec => spec.path));
|
|
517
600
|
const unexpected = changeset.filter(c => !c.before && c.after && !expectedPaths.has(c.after.sourcePath));
|
|
518
601
|
if (unexpected.length > 0) {
|
|
519
602
|
throw new Error([
|
|
@@ -554,7 +637,7 @@ export class RecipeSpec {
|
|
|
554
637
|
let unnamed = 0;
|
|
555
638
|
const before: [SourceSpec<any>, { text: string, sourcePath: string }][] = [];
|
|
556
639
|
for (const spec of specs) {
|
|
557
|
-
if (spec.before) {
|
|
640
|
+
if (spec.before !== null) {
|
|
558
641
|
const sourcePath = spec.path || `source-${unnamed++}.${spec.ext}`;
|
|
559
642
|
before.push([spec, {text: dedent(spec.before), sourcePath: sourcePath}]);
|
|
560
643
|
}
|
|
@@ -587,6 +670,76 @@ class ValidateWhitespaceVisitor extends JavaScriptVisitor<ExecutionContext> {
|
|
|
587
670
|
}
|
|
588
671
|
}
|
|
589
672
|
|
|
673
|
+
/** Whether the framework puts `key` on the execution context of a run itself. */
|
|
674
|
+
function isFrameworkMessage(key: string | symbol): boolean {
|
|
675
|
+
return key === DATA_TABLE_STORE || key === ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Records the messages that a recipe puts on the execution context of its run, with the code that put each. */
|
|
679
|
+
class ExecutionContextWrites {
|
|
680
|
+
private readonly writes = new Map<string | symbol, string | undefined>();
|
|
681
|
+
|
|
682
|
+
/** A view of `ctx` whose messages record what is put on them, and that otherwise acts as `ctx`. */
|
|
683
|
+
watch(ctx: ExecutionContext): ExecutionContext {
|
|
684
|
+
const record = (key: string | symbol) => {
|
|
685
|
+
if (!isFrameworkMessage(key) && !this.writes.has(key)) {
|
|
686
|
+
this.writes.set(key, callerFrame());
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
const messages = new Proxy(ctx.messages, {
|
|
690
|
+
set(target, key, value) {
|
|
691
|
+
record(key);
|
|
692
|
+
return Reflect.set(target, key, value);
|
|
693
|
+
},
|
|
694
|
+
defineProperty(target, key, attributes) {
|
|
695
|
+
record(key);
|
|
696
|
+
return Reflect.defineProperty(target, key, attributes);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
return Object.create(ctx, {messages: {value: messages, enumerable: true, writable: true}});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
assertNone(recipe: Recipe): void {
|
|
703
|
+
if (this.writes.size === 0) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
throw new Error([
|
|
707
|
+
"Keep state that spans source files in a ScanningRecipe's accumulator, and state within one source file " +
|
|
708
|
+
"in cursor messages, with this.cursor.messages.set(key, value). " +
|
|
709
|
+
`${recipe.name} put messages on the ExecutionContext, which every recipe and source file in a run ` +
|
|
710
|
+
"shares, so what it keeps there leaks into the others:",
|
|
711
|
+
...[...this.writes].map(([key, frame]) =>
|
|
712
|
+
` ${typeof key === "symbol" ? key.toString() : JSON.stringify(key)}${frame ? ` at ${frame}` : ""}`),
|
|
713
|
+
"A recipe that must change the ExecutionContext opts out with spec.checkExecutionContextUnchanged = false."
|
|
714
|
+
].join("\n"));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** The first frame of the stack outside the harness and the runtime, relative to the working directory. */
|
|
719
|
+
function callerFrame(): string | undefined {
|
|
720
|
+
const frame = (new Error().stack ?? "").split("\n").slice(1)
|
|
721
|
+
.map(line => line.trim().replace(/^at (async )?/, ""))
|
|
722
|
+
.find(line => !/[\\/]test[\\/]rewrite-test\.[jt]s:|<anonymous>|^node:/.test(line));
|
|
723
|
+
return frame?.split(process.cwd() + path.sep).join("");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Whether the recipe is run on sources the test doesn't give it, which a recipe isn't when it or one in its recipe list
|
|
728
|
+
* is the harness's stand-in for a visitor, or runs in Java, whose harness probes it.
|
|
729
|
+
*/
|
|
730
|
+
async function isProbed(recipe: Recipe): Promise<boolean> {
|
|
731
|
+
if (recipe instanceof NoopRecipe || recipe instanceof AdHocRecipe || recipe instanceof RpcRecipe ||
|
|
732
|
+
recipe instanceof DelegatingRecipe) {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
for (const child of await recipe.recipeList()) {
|
|
736
|
+
if (!await isProbed(child)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
|
|
590
743
|
/** Without the comments that search results and markup print as. */
|
|
591
744
|
function printSanitized(sourceFile: SourceFile): Promise<string> {
|
|
592
745
|
return TreePrinters.printer(sourceFile).print(sourceFile, new PrintOutputCapture(MarkerPrinter.SANITIZED));
|
|
@@ -0,0 +1,332 @@
|
|
|
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 {Recipe} from "../recipe";
|
|
17
|
+
import {Cursor, isTree, rootCursor, SourceFile, Tree} from "../tree";
|
|
18
|
+
import {Result} from "../run";
|
|
19
|
+
import {MarkerPrinter, PrintOutputCapture, TreePrinters} from "../print";
|
|
20
|
+
import {isParseError} from "../parse-error";
|
|
21
|
+
import {MarkersKind, ParseExceptionResult} from "../markers";
|
|
22
|
+
import {TreeVisitor} from "../visitor";
|
|
23
|
+
import {J} from "../java";
|
|
24
|
+
import {JavaScriptVisitor, JS} from "../javascript";
|
|
25
|
+
import {Json, JsonVisitor} from "../json";
|
|
26
|
+
import {Yaml, YamlVisitor} from "../yaml";
|
|
27
|
+
import {typeName} from "./probes";
|
|
28
|
+
|
|
29
|
+
const MAX_PER_FILE = 5;
|
|
30
|
+
const SNIPPET_LENGTH = 60;
|
|
31
|
+
const STRICT_NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/;
|
|
32
|
+
|
|
33
|
+
/** How the check reads the trees of a language, as Java's `TreeShapeValidationService` does. */
|
|
34
|
+
interface Language {
|
|
35
|
+
/** The visitor of the language, whose order of visiting the check compares nodes in. */
|
|
36
|
+
readonly visitor: abstract new () => TreeVisitor<any, any>
|
|
37
|
+
|
|
38
|
+
/** What tells a node apart from another of its kind with the same children, such as an identifier's name. */
|
|
39
|
+
value(tree: Tree): string | undefined
|
|
40
|
+
|
|
41
|
+
/** Whether the comparison leaves out a node and what it holds, as with an empty element the parser adds. */
|
|
42
|
+
skip(tree: Tree): boolean
|
|
43
|
+
|
|
44
|
+
/** What a node has that the parser accepts but the language doesn't, such as a duplicate key. */
|
|
45
|
+
invalid(tree: Tree): string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let languages: { [kind: string]: Language } | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The language of a source file of `kind`, from a table built on first use, since the modules of the languages import
|
|
52
|
+
* the harness and may still be loading when it loads.
|
|
53
|
+
*/
|
|
54
|
+
function languageOf(kind: string): Language | undefined {
|
|
55
|
+
languages ??= {
|
|
56
|
+
[JS.Kind.CompilationUnit]: {
|
|
57
|
+
visitor: JavaScriptVisitor,
|
|
58
|
+
value: javaScriptValue,
|
|
59
|
+
skip: tree => tree.kind === J.Kind.Empty,
|
|
60
|
+
invalid: () => []
|
|
61
|
+
},
|
|
62
|
+
[Json.Kind.Document]: {
|
|
63
|
+
visitor: JsonVisitor,
|
|
64
|
+
value: jsonValue,
|
|
65
|
+
skip: tree => tree.kind === Json.Kind.Empty,
|
|
66
|
+
invalid: jsonInvalid
|
|
67
|
+
},
|
|
68
|
+
[Yaml.Kind.Documents]: {
|
|
69
|
+
visitor: YamlVisitor,
|
|
70
|
+
value: yamlValue,
|
|
71
|
+
skip: () => false,
|
|
72
|
+
invalid: yamlInvalid
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return languages[kind];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Node {
|
|
79
|
+
readonly tree: Tree
|
|
80
|
+
readonly cursor: Cursor
|
|
81
|
+
readonly children: Node[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Finds nodes in a recipe's output that its parser builds differently from the text they print, which recipes that
|
|
86
|
+
* run after it don't match the way they match parsed code.
|
|
87
|
+
*/
|
|
88
|
+
export class TreeShapes {
|
|
89
|
+
constructor(private readonly recipe: Recipe) {
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param freshParse The fresh parse of the text each source the recipe left prints. */
|
|
93
|
+
async assertParserShaped(results: Result[], freshParse: Map<SourceFile, SourceFile>): Promise<void> {
|
|
94
|
+
const found: string[] = [];
|
|
95
|
+
for (const {before, after} of results) {
|
|
96
|
+
const language = after && languageOf(after.kind);
|
|
97
|
+
if (!after || after === before || !language) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const text = await printed(after);
|
|
101
|
+
const reparsed = freshParse.get(after);
|
|
102
|
+
if (!reparsed || text.trim() === "" || before && await printed(before) === text) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const problems = await this.problems(language, after, before, reparsed, text);
|
|
106
|
+
if (problems.length > 0) {
|
|
107
|
+
found.push(` ${after.sourcePath}:`, ...problems.map(problem => ` ${problem}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (found.length > 0) {
|
|
111
|
+
throw new Error([
|
|
112
|
+
"Write code with a template or the format's own parser, or build the nodes the way its parser " +
|
|
113
|
+
`builds the text they print. The output of ${this.recipe.name} holds nodes that its parser makes ` +
|
|
114
|
+
"differently of the same text:",
|
|
115
|
+
...found,
|
|
116
|
+
"A recipe that runs after it matches the tree, not the text, so it misses or mistakes a node that " +
|
|
117
|
+
"parsed code never holds. If the output is meant to differ from parsed code, set " +
|
|
118
|
+
"spec.checkParserShapedTrees = false."
|
|
119
|
+
].join("\n"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async problems(language: Language, after: SourceFile, before: SourceFile | undefined,
|
|
124
|
+
reparsed: SourceFile, text: string): Promise<string[]> {
|
|
125
|
+
if (isParseError(reparsed)) {
|
|
126
|
+
const exception = reparsed.markers.markers
|
|
127
|
+
.find(m => m.kind === MarkersKind.ParseExceptionResult) as ParseExceptionResult | undefined;
|
|
128
|
+
return [`the text it prints doesn't parse: ${exception ? firstLine(exception.message) : "a ParseError"}`];
|
|
129
|
+
} else if (reparsed.kind !== after.kind || await printed(reparsed) !== text) {
|
|
130
|
+
// a parser that doesn't print back what it read is the parser's to fix
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const problems: string[] = [];
|
|
134
|
+
const shape = await skeleton(language, after);
|
|
135
|
+
await compare(language, after, shape, await skeleton(language, reparsed), problems);
|
|
136
|
+
// a file that already broke its language's rules follows other ones
|
|
137
|
+
if (!before || invalid(language, await skeleton(language, before)).length === 0) {
|
|
138
|
+
for (const problem of invalid(language, shape)) {
|
|
139
|
+
if (problems.length < MAX_PER_FILE) {
|
|
140
|
+
problems.push(problem);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return problems;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function compare(language: Language, sourceFile: SourceFile, node: Node, parsed: Node,
|
|
149
|
+
problems: string[]): Promise<void> {
|
|
150
|
+
if (problems.length >= MAX_PER_FILE) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (node.tree.kind !== parsed.tree.kind) {
|
|
154
|
+
problems.push(`${await snippet(sourceFile, node)} is a ${typeName(node.tree.kind)} where the parser makes a ` +
|
|
155
|
+
typeName(parsed.tree.kind));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const value = language.value(node.tree);
|
|
159
|
+
const parsedValue = language.value(parsed.tree);
|
|
160
|
+
if (value !== parsedValue) {
|
|
161
|
+
problems.push(`${await snippet(sourceFile, node)} has ${value} where the parser makes ${parsedValue}`);
|
|
162
|
+
}
|
|
163
|
+
if (node.children.length !== parsed.children.length) {
|
|
164
|
+
problems.push(`${await snippet(sourceFile, node)} holds ${kinds(node.children)} where the parser makes ` +
|
|
165
|
+
kinds(parsed.children));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
169
|
+
await compare(language, sourceFile, node.children[i], parsed.children[i], problems);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function kinds(nodes: Node[]): string {
|
|
174
|
+
return nodes.length === 0 ? "nothing" : nodes.map(node => typeName(node.tree.kind)).join(", ");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The nodes of a source file, each with the nodes it holds, leaving out those the language skips. */
|
|
178
|
+
async function skeleton(language: Language, sourceFile: SourceFile): Promise<Node> {
|
|
179
|
+
const roots: Node[] = [];
|
|
180
|
+
const path: Node[] = [];
|
|
181
|
+
const Visitor = language.visitor as unknown as new () => TreeVisitor<any, any>;
|
|
182
|
+
const visitor = new class extends Visitor {
|
|
183
|
+
protected override async preVisit(tree: Tree, _p: any): Promise<Tree | undefined> {
|
|
184
|
+
if (language.skip(tree)) {
|
|
185
|
+
this.stopAfterPreVisit();
|
|
186
|
+
return tree;
|
|
187
|
+
}
|
|
188
|
+
const ancestors = this.cursor.asArray().filter(value => isTree(value)).length - 1;
|
|
189
|
+
path.length = Math.min(path.length, ancestors);
|
|
190
|
+
const node: Node = {tree, cursor: this.cursor, children: []};
|
|
191
|
+
(path.length === 0 ? roots : path[path.length - 1].children).push(node);
|
|
192
|
+
path.push(node);
|
|
193
|
+
return tree;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
await visitor.visit(sourceFile, undefined);
|
|
197
|
+
return roots[0] ?? {tree: sourceFile, cursor: rootCursor(), children: []};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function invalid(language: Language, node: Node): string[] {
|
|
201
|
+
return [...language.invalid(node.tree), ...node.children.flatMap(child => invalid(language, child))];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function snippet(sourceFile: SourceFile, node: Node): Promise<string> {
|
|
205
|
+
let shown: string;
|
|
206
|
+
try {
|
|
207
|
+
shown = (await TreePrinters.printer(sourceFile)
|
|
208
|
+
.print(node.tree, new PrintOutputCapture(MarkerPrinter.SANITIZED), node.cursor.parent))
|
|
209
|
+
.trim().replace(/\s+/g, " ");
|
|
210
|
+
} catch {
|
|
211
|
+
shown = "?";
|
|
212
|
+
}
|
|
213
|
+
return `\`${shown.length > SNIPPET_LENGTH ? `${shown.substring(0, SNIPPET_LENGTH)}...` : shown}\``;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function javaScriptValue(tree: Tree): string | undefined {
|
|
217
|
+
switch (tree.kind) {
|
|
218
|
+
case J.Kind.Identifier:
|
|
219
|
+
return `the name \`${(tree as J.Identifier).simpleName}\``;
|
|
220
|
+
case J.Kind.Literal: {
|
|
221
|
+
const literal = tree as J.Literal;
|
|
222
|
+
// the parser keeps the source of a literal with surrogate escapes in place of its value
|
|
223
|
+
const value = literal.unicodeEscapes?.length ? "a value with surrogate escapes" :
|
|
224
|
+
literal.value === null || literal.value === undefined ? `the value ${literal.value}` :
|
|
225
|
+
`the ${typeof literal.value} value ${quoted(literal.value)}`;
|
|
226
|
+
return `${value} of type ${literal.type?.keyword ?? "none"}`;
|
|
227
|
+
}
|
|
228
|
+
case J.Kind.Modifier: {
|
|
229
|
+
const modifier = tree as J.Modifier;
|
|
230
|
+
return `the modifier ${modifier.type === J.ModifierType.LanguageExtension ? modifier.keyword : modifier.type}`;
|
|
231
|
+
}
|
|
232
|
+
case J.Kind.Primitive:
|
|
233
|
+
return `the type ${(tree as J.Primitive).type.keyword}`;
|
|
234
|
+
case J.Kind.Binary:
|
|
235
|
+
case J.Kind.Unary:
|
|
236
|
+
case J.Kind.AssignmentOperation:
|
|
237
|
+
case JS.Kind.Binary:
|
|
238
|
+
case JS.Kind.AssignmentOperation:
|
|
239
|
+
return `the operator ${(tree as J.Binary).operator.element}`;
|
|
240
|
+
case JS.Kind.TypeOperator:
|
|
241
|
+
return `the operator ${(tree as JS.TypeOperator).operator}`;
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function quoted(value: string | number | bigint | boolean): string {
|
|
247
|
+
let shown = String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r")
|
|
248
|
+
.replace(/\t/g, "\\t");
|
|
249
|
+
if (shown.length > 40) {
|
|
250
|
+
shown = `${shown.substring(0, 40)}...`;
|
|
251
|
+
}
|
|
252
|
+
return typeof value === "string" ? `"${shown}"` : shown;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function jsonValue(tree: Tree): string | undefined {
|
|
256
|
+
if (tree.kind === Json.Kind.Literal) {
|
|
257
|
+
const value = (tree as Json.Literal).value;
|
|
258
|
+
return value === null || value === undefined ? `the value ${value}` : `the ${typeof value} value \`${value}\``;
|
|
259
|
+
} else if (tree.kind === Json.Kind.Identifier) {
|
|
260
|
+
return `the name \`${(tree as Json.Identifier).name}\``;
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function jsonInvalid(tree: Tree): string[] {
|
|
266
|
+
const found: string[] = [];
|
|
267
|
+
if (tree.kind === Json.Kind.Object) {
|
|
268
|
+
const keys = new Set<string>();
|
|
269
|
+
for (const member of (tree as Json.Object).members.map(m => m.element)) {
|
|
270
|
+
if (member.kind === Json.Kind.Member) {
|
|
271
|
+
const key = (member as Json.Member).key.element;
|
|
272
|
+
const name = key.kind === Json.Kind.Identifier ? key.name : String(key.value);
|
|
273
|
+
if (keys.has(name)) {
|
|
274
|
+
found.push(`the key \`${name}\` appears twice in one object`);
|
|
275
|
+
}
|
|
276
|
+
keys.add(name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else if (tree.kind === Json.Kind.Member) {
|
|
280
|
+
const key = (tree as Json.Member).key.element;
|
|
281
|
+
if (key.kind === Json.Kind.Identifier) {
|
|
282
|
+
found.push(`the key \`${key.name}\` has no quotes, which JSON doesn't allow`);
|
|
283
|
+
}
|
|
284
|
+
} else if (tree.kind === Json.Kind.Literal) {
|
|
285
|
+
const {source, value} = tree as Json.Literal;
|
|
286
|
+
if (source.startsWith("'")) {
|
|
287
|
+
found.push(`\`${source}\` is in single quotes, which JSON doesn't allow`);
|
|
288
|
+
} else if (typeof value === "number" && !STRICT_NUMBER.test(source)) {
|
|
289
|
+
found.push(`\`${source}\` isn't a number JSON allows`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return found;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function yamlValue(tree: Tree): string | undefined {
|
|
296
|
+
if (tree.kind === Yaml.Kind.Scalar) {
|
|
297
|
+
const scalar = tree as Yaml.Scalar;
|
|
298
|
+
// a block scalar's value holds the whitespace up to the next node, which the parser splits differently
|
|
299
|
+
const block = scalar.style === Yaml.ScalarStyle.LITERAL || scalar.style === Yaml.ScalarStyle.FOLDED;
|
|
300
|
+
return `the ${scalar.style} value \`${block ? scalar.value.replace(/\s+$/, "") : scalar.value}\``;
|
|
301
|
+
} else if (tree.kind === Yaml.Kind.Anchor) {
|
|
302
|
+
return `the anchor \`${(tree as Yaml.Anchor).key}\``;
|
|
303
|
+
} else if (tree.kind === Yaml.Kind.Alias) {
|
|
304
|
+
return `the alias of \`${(tree as Yaml.Alias).anchor.key}\``;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function yamlInvalid(tree: Tree): string[] {
|
|
310
|
+
if (tree.kind !== Yaml.Kind.Mapping) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
const found: string[] = [];
|
|
314
|
+
const keys = new Set<string>();
|
|
315
|
+
for (const {key} of (tree as Yaml.Mapping).entries) {
|
|
316
|
+
const name = key.kind === Yaml.Kind.Scalar ? (key as Yaml.Scalar).value : `*${(key as Yaml.Alias).anchor.key}`;
|
|
317
|
+
if (keys.has(name)) {
|
|
318
|
+
found.push(`the key \`${name}\` appears twice in one mapping, which YAML doesn't allow`);
|
|
319
|
+
}
|
|
320
|
+
keys.add(name);
|
|
321
|
+
}
|
|
322
|
+
return found;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function printed(sourceFile: SourceFile): Promise<string> {
|
|
326
|
+
return TreePrinters.printer(sourceFile).print(sourceFile, new PrintOutputCapture(MarkerPrinter.SANITIZED));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function firstLine(message: string): string {
|
|
330
|
+
const line = message.trim().split("\n")[0];
|
|
331
|
+
return line.length > 200 ? `${line.substring(0, 200)}...` : line;
|
|
332
|
+
}
|