@ontrails/regrade 1.0.0-beta.39 → 1.0.0-beta.41
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/CHANGELOG.md +81 -0
- package/package.json +6 -5
- package/src/downstream/ast-rewrite.ts +567 -54
- package/src/downstream/collect.ts +1 -1
- package/src/downstream/export-restructure.ts +2 -2
- package/src/downstream/report.ts +140 -1
- package/src/downstream/vocabulary-registry.ts +4 -6
- package/src/downstream/vocabulary.ts +97 -23
- package/src/literal-transform.ts +26 -20
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AstNode,
|
|
3
|
-
AstScopeContext,
|
|
4
|
-
SourceEdit,
|
|
5
|
-
} from '@ontrails/warden/ast';
|
|
1
|
+
import type { AstNode, AstScopeContext, SourceEdit } from '@ontrails/source';
|
|
6
2
|
import {
|
|
7
3
|
applySourceEdits,
|
|
8
4
|
createSourceEdit,
|
|
5
|
+
extractPlainTemplateLiteral,
|
|
6
|
+
getNodeCallee,
|
|
9
7
|
getStringValue,
|
|
10
8
|
identifierName,
|
|
9
|
+
isMemberExpression,
|
|
11
10
|
isStringLiteral,
|
|
12
11
|
offsetToLineColumn,
|
|
13
12
|
parseWithDiagnostics,
|
|
14
13
|
validateSourceEdits,
|
|
15
14
|
walkWithScopeContext,
|
|
16
|
-
} from '@ontrails/
|
|
15
|
+
} from '@ontrails/source';
|
|
17
16
|
import type { GovernedVocabularyTransition } from '@ontrails/warden';
|
|
18
17
|
|
|
19
18
|
import type {
|
|
@@ -23,8 +22,8 @@ import type {
|
|
|
23
22
|
RegradeReviewDetail,
|
|
24
23
|
} from './report.js';
|
|
25
24
|
|
|
26
|
-
export interface AstRewriteContext
|
|
27
|
-
|
|
25
|
+
export interface AstRewriteContext
|
|
26
|
+
extends AstScopeContext, RegradeClassContext {
|
|
28
27
|
readonly source: string;
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -179,7 +178,7 @@ export const createAstRewriteClass = (
|
|
|
179
178
|
...toArray(
|
|
180
179
|
options.visit(node, {
|
|
181
180
|
...scopeContext,
|
|
182
|
-
|
|
181
|
+
...context,
|
|
183
182
|
source,
|
|
184
183
|
})
|
|
185
184
|
)
|
|
@@ -233,13 +232,17 @@ export interface AstIdentifierRenameClassOptions {
|
|
|
233
232
|
readonly describe?: string;
|
|
234
233
|
readonly from: string;
|
|
235
234
|
readonly id?: string;
|
|
235
|
+
readonly match?: AstIdentifierRenameMatchMode;
|
|
236
236
|
readonly reviewDeclarationTypes?: ReadonlySet<string>;
|
|
237
|
+
readonly reviewExistingTargetSegments?: readonly string[];
|
|
237
238
|
readonly shouldPreserve?: (
|
|
238
239
|
occurrence: AstIdentifierRenameOccurrence
|
|
239
240
|
) => boolean;
|
|
240
241
|
readonly to: string;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
export type AstIdentifierRenameMatchMode = 'exact' | 'identifier-segment';
|
|
245
|
+
|
|
243
246
|
export interface AstIdentifierRenameOccurrence {
|
|
244
247
|
readonly end: number;
|
|
245
248
|
readonly from: string;
|
|
@@ -250,18 +253,111 @@ export interface AstIdentifierRenameOccurrence {
|
|
|
250
253
|
}
|
|
251
254
|
|
|
252
255
|
export interface AstStringLiteralRenameClassOptions {
|
|
256
|
+
readonly allowModuleSpecifier?: boolean;
|
|
253
257
|
readonly describe?: string;
|
|
254
258
|
readonly from: string;
|
|
255
259
|
readonly id?: string;
|
|
260
|
+
readonly match?: 'exact' | 'property-key' | 'review';
|
|
256
261
|
readonly shouldPreserve?: (
|
|
257
262
|
occurrence: AstIdentifierRenameOccurrence
|
|
258
263
|
) => boolean;
|
|
264
|
+
/** Require the owning manifest to already admit this package route. */
|
|
265
|
+
readonly targetPackage?: string;
|
|
259
266
|
readonly to: string;
|
|
260
267
|
}
|
|
261
268
|
|
|
269
|
+
const isEsmModuleSpecifierPosition = (context: AstScopeContext): boolean =>
|
|
270
|
+
context.key === 'source' &&
|
|
271
|
+
(context.parent?.type === 'ImportDeclaration' ||
|
|
272
|
+
context.parent?.type === 'ExportAllDeclaration' ||
|
|
273
|
+
context.parent?.type === 'ExportNamedDeclaration' ||
|
|
274
|
+
context.parent?.type === 'ImportExpression' ||
|
|
275
|
+
context.parent?.type === 'TSImportType');
|
|
276
|
+
|
|
277
|
+
const isTypeScriptModuleSpecifierPosition = (
|
|
278
|
+
context: AstScopeContext
|
|
279
|
+
): boolean =>
|
|
280
|
+
(context.key === 'id' && context.parent?.type === 'TSModuleDeclaration') ||
|
|
281
|
+
(context.key === 'expression' &&
|
|
282
|
+
context.parent?.type === 'TSExternalModuleReference');
|
|
283
|
+
|
|
284
|
+
const memberAccessPath = (node: AstNode | undefined): string | null => {
|
|
285
|
+
const name = identifierName(node);
|
|
286
|
+
if (name !== null) {
|
|
287
|
+
return name;
|
|
288
|
+
}
|
|
289
|
+
if (!isMemberExpression(node)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const object = memberAccessPath(node.object);
|
|
293
|
+
const property = identifierName(node.property);
|
|
294
|
+
return object === null || property === null ? null : `${object}.${property}`;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const MODULE_LOADER_CALLEES = new Set([
|
|
298
|
+
'Bun.mock.module',
|
|
299
|
+
'jest.createMockFromModule',
|
|
300
|
+
'jest.doMock',
|
|
301
|
+
'jest.genMockFromModule',
|
|
302
|
+
'jest.mock',
|
|
303
|
+
'jest.requireActual',
|
|
304
|
+
'jest.requireMock',
|
|
305
|
+
'jest.unmock',
|
|
306
|
+
'mock.module',
|
|
307
|
+
'require',
|
|
308
|
+
'require.resolve',
|
|
309
|
+
'vi.doMock',
|
|
310
|
+
'vi.doUnmock',
|
|
311
|
+
'vi.importActual',
|
|
312
|
+
'vi.importMock',
|
|
313
|
+
'vi.mock',
|
|
314
|
+
'vi.unmock',
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const isLoaderModuleSpecifierPosition = (context: AstScopeContext): boolean => {
|
|
318
|
+
if (
|
|
319
|
+
context.key !== 'arguments' ||
|
|
320
|
+
context.parent?.type !== 'CallExpression'
|
|
321
|
+
) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const callee = memberAccessPath(getNodeCallee(context.parent));
|
|
326
|
+
return callee !== null && MODULE_LOADER_CALLEES.has(callee);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const isModuleSpecifierPosition = (context: AstScopeContext): boolean =>
|
|
330
|
+
isEsmModuleSpecifierPosition(context) ||
|
|
331
|
+
isTypeScriptModuleSpecifierPosition(context) ||
|
|
332
|
+
isLoaderModuleSpecifierPosition(context);
|
|
333
|
+
|
|
334
|
+
const isTargetPackageMissing = (
|
|
335
|
+
context: AstRewriteContext,
|
|
336
|
+
targetPackage: string | undefined
|
|
337
|
+
): boolean => {
|
|
338
|
+
if (targetPackage === undefined || context.package?.name === targetPackage) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const testSource =
|
|
342
|
+
context.path.split('/').includes('__tests__') ||
|
|
343
|
+
/(?:^|\.)(?:test|spec)\.[cm]?[jt]sx?$/.test(
|
|
344
|
+
context.path.split('/').at(-1) ?? ''
|
|
345
|
+
);
|
|
346
|
+
const dependencies = testSource
|
|
347
|
+
? context.package?.dependencies
|
|
348
|
+
: (context.package?.runtimeDependencies ?? context.package?.dependencies);
|
|
349
|
+
return !dependencies?.includes(targetPackage);
|
|
350
|
+
};
|
|
351
|
+
|
|
262
352
|
const isIdentifierNamed = (node: AstNode, name: string): boolean =>
|
|
263
353
|
node.type === 'Identifier' && identifierName(node) === name;
|
|
264
354
|
|
|
355
|
+
const stringLiteralNeedsReview = (
|
|
356
|
+
match: AstStringLiteralRenameClassOptions['match'],
|
|
357
|
+
contextKey: AstScopeContext['key']
|
|
358
|
+
): boolean =>
|
|
359
|
+
match === 'review' || (match === 'property-key' && contextKey !== 'key');
|
|
360
|
+
|
|
265
361
|
const identifierTokenSpan = (
|
|
266
362
|
node: AstNode,
|
|
267
363
|
source: string,
|
|
@@ -273,9 +369,170 @@ const identifierTokenSpan = (
|
|
|
273
369
|
: null;
|
|
274
370
|
};
|
|
275
371
|
|
|
372
|
+
const toPascalIdentifierSegment = (value: string): string =>
|
|
373
|
+
value.length === 0
|
|
374
|
+
? value
|
|
375
|
+
: `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}`;
|
|
376
|
+
|
|
377
|
+
const isScreamingSnakeIdentifier = (name: string): boolean =>
|
|
378
|
+
/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/.test(name);
|
|
379
|
+
|
|
380
|
+
const replaceScreamingSnakeIdentifierSegment = (
|
|
381
|
+
name: string,
|
|
382
|
+
from: string,
|
|
383
|
+
to: string
|
|
384
|
+
): string | null => {
|
|
385
|
+
if (!isScreamingSnakeIdentifier(name)) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const fromSegment = from.toUpperCase();
|
|
390
|
+
const toSegment = to.toUpperCase();
|
|
391
|
+
let changed = false;
|
|
392
|
+
const nextName = name
|
|
393
|
+
.split('_')
|
|
394
|
+
.map((segment) => {
|
|
395
|
+
if (segment !== fromSegment) {
|
|
396
|
+
return segment;
|
|
397
|
+
}
|
|
398
|
+
changed = true;
|
|
399
|
+
return toSegment;
|
|
400
|
+
})
|
|
401
|
+
.join('_');
|
|
402
|
+
|
|
403
|
+
return changed ? nextName : null;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const replaceCamelOrPascalIdentifierSegment = (
|
|
407
|
+
name: string,
|
|
408
|
+
from: string,
|
|
409
|
+
to: string
|
|
410
|
+
): string | null => {
|
|
411
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name)) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const fromPascalSegment = toPascalIdentifierSegment(from);
|
|
416
|
+
const toPascalSegment = toPascalIdentifierSegment(to);
|
|
417
|
+
const segmentPattern = /[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z]+|[0-9]+/g;
|
|
418
|
+
let cursor = 0;
|
|
419
|
+
let changed = false;
|
|
420
|
+
let nextName = '';
|
|
421
|
+
|
|
422
|
+
for (const match of name.matchAll(segmentPattern)) {
|
|
423
|
+
const [segment] = match;
|
|
424
|
+
const { index } = match;
|
|
425
|
+
if (index === undefined || index !== cursor) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (segment === from) {
|
|
430
|
+
changed = true;
|
|
431
|
+
nextName += to;
|
|
432
|
+
} else if (segment === fromPascalSegment) {
|
|
433
|
+
changed = true;
|
|
434
|
+
nextName += toPascalSegment;
|
|
435
|
+
} else {
|
|
436
|
+
nextName += segment;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
cursor = index + segment.length;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (cursor !== name.length || !changed) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return nextName;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const deriveIdentifierSegmentTarget = (
|
|
450
|
+
name: string,
|
|
451
|
+
from: string,
|
|
452
|
+
to: string
|
|
453
|
+
): string | null => {
|
|
454
|
+
const leadingUnderscores = /^_+/.exec(name)?.[0] ?? '';
|
|
455
|
+
const coreName =
|
|
456
|
+
leadingUnderscores.length > 0
|
|
457
|
+
? name.slice(leadingUnderscores.length)
|
|
458
|
+
: name;
|
|
459
|
+
if (coreName.length === 0) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const screamingSnakeTarget = replaceScreamingSnakeIdentifierSegment(
|
|
464
|
+
coreName,
|
|
465
|
+
from,
|
|
466
|
+
to
|
|
467
|
+
);
|
|
468
|
+
if (screamingSnakeTarget !== null) {
|
|
469
|
+
return `${leadingUnderscores}${screamingSnakeTarget}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const camelOrPascalTarget = replaceCamelOrPascalIdentifierSegment(
|
|
473
|
+
coreName,
|
|
474
|
+
from,
|
|
475
|
+
to
|
|
476
|
+
);
|
|
477
|
+
return camelOrPascalTarget === null
|
|
478
|
+
? null
|
|
479
|
+
: `${leadingUnderscores}${camelOrPascalTarget}`;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const hasIdentifierSegment = (name: string, segment: string): boolean =>
|
|
483
|
+
deriveIdentifierSegmentTarget(name, segment, segment) !== null;
|
|
484
|
+
|
|
485
|
+
interface AstIdentifierRenameMatch {
|
|
486
|
+
readonly from: string;
|
|
487
|
+
readonly span: { readonly end: number; readonly start: number } | null;
|
|
488
|
+
readonly to: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const resolveIdentifierRenameMatch = (
|
|
492
|
+
node: AstNode,
|
|
493
|
+
source: string,
|
|
494
|
+
options: {
|
|
495
|
+
readonly from: string;
|
|
496
|
+
readonly match: AstIdentifierRenameMatchMode;
|
|
497
|
+
readonly to: string;
|
|
498
|
+
}
|
|
499
|
+
): AstIdentifierRenameMatch | null => {
|
|
500
|
+
if (options.match === 'exact') {
|
|
501
|
+
if (!isIdentifierNamed(node, options.from)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
from: options.from,
|
|
507
|
+
span: identifierTokenSpan(node, source, options.from),
|
|
508
|
+
to: options.to,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (node.type !== 'Identifier') {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const name = identifierName(node);
|
|
517
|
+
if (name === null) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const target = deriveIdentifierSegmentTarget(name, options.from, options.to);
|
|
521
|
+
if (target === null) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
from: name,
|
|
527
|
+
span: identifierTokenSpan(node, source, name),
|
|
528
|
+
to: target,
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
|
|
276
532
|
export const createAstIdentifierRenameClass = (
|
|
277
533
|
options: AstIdentifierRenameClassOptions
|
|
278
534
|
): RegradeClass => {
|
|
535
|
+
const matchMode = options.match ?? 'exact';
|
|
279
536
|
const reviewDeclarationTypes =
|
|
280
537
|
options.reviewDeclarationTypes ?? new Set<string>();
|
|
281
538
|
|
|
@@ -285,20 +542,25 @@ export const createAstIdentifierRenameClass = (
|
|
|
285
542
|
`Rename identifier "${options.from}" to "${options.to}".`,
|
|
286
543
|
id: options.id ?? `ast-identifier-rename:${options.from}->${options.to}`,
|
|
287
544
|
visit: (node, context) => {
|
|
288
|
-
|
|
545
|
+
const match = resolveIdentifierRenameMatch(node, context.source, {
|
|
546
|
+
from: options.from,
|
|
547
|
+
match: matchMode,
|
|
548
|
+
to: options.to,
|
|
549
|
+
});
|
|
550
|
+
if (match === null) {
|
|
289
551
|
return null;
|
|
290
552
|
}
|
|
291
553
|
|
|
292
|
-
const span =
|
|
554
|
+
const { span } = match;
|
|
293
555
|
if (span === null) {
|
|
294
556
|
const location = offsetToLineColumn(context.source, node.start);
|
|
295
|
-
const caution = `Identifier "${
|
|
557
|
+
const caution = `Identifier "${match.from}" token span could not be verified; routed to review.`;
|
|
296
558
|
return {
|
|
297
559
|
detail: {
|
|
298
|
-
candidateReplacement:
|
|
299
|
-
expectedTarget: `Rename identifier "${
|
|
560
|
+
candidateReplacement: match.to,
|
|
561
|
+
expectedTarget: `Rename identifier "${match.from}" to "${match.to}".`,
|
|
300
562
|
judgment: 'unresolved',
|
|
301
|
-
matchedForm:
|
|
563
|
+
matchedForm: match.from,
|
|
302
564
|
nodeKind: node.type,
|
|
303
565
|
preserveCautions: [caution],
|
|
304
566
|
reason: 'ast-identifier-token-span-unverified',
|
|
@@ -310,7 +572,7 @@ export const createAstIdentifierRenameClass = (
|
|
|
310
572
|
start: node.start,
|
|
311
573
|
},
|
|
312
574
|
suggestedValidation: 'bun run typecheck',
|
|
313
|
-
symbol:
|
|
575
|
+
symbol: match.from,
|
|
314
576
|
},
|
|
315
577
|
kind: 'review',
|
|
316
578
|
note: caution,
|
|
@@ -321,26 +583,60 @@ export const createAstIdentifierRenameClass = (
|
|
|
321
583
|
if (
|
|
322
584
|
options.shouldPreserve?.({
|
|
323
585
|
end: span.end,
|
|
324
|
-
from:
|
|
586
|
+
from: match.from,
|
|
325
587
|
path: context.path,
|
|
326
588
|
source: context.source,
|
|
327
589
|
start: span.start,
|
|
328
|
-
to:
|
|
590
|
+
to: match.to,
|
|
329
591
|
}) === true
|
|
330
592
|
) {
|
|
331
593
|
return null;
|
|
332
594
|
}
|
|
333
595
|
|
|
334
|
-
const
|
|
596
|
+
const existingTargetSegment = (
|
|
597
|
+
options.reviewExistingTargetSegments ?? [options.to]
|
|
598
|
+
).find((segment) => hasIdentifierSegment(match.from, segment));
|
|
599
|
+
if (
|
|
600
|
+
matchMode === 'identifier-segment' &&
|
|
601
|
+
existingTargetSegment !== undefined
|
|
602
|
+
) {
|
|
603
|
+
const location = offsetToLineColumn(context.source, span.start);
|
|
604
|
+
const caution = `Identifier "${match.from}" already contains target segment "${existingTargetSegment}"; routed to review.`;
|
|
605
|
+
return {
|
|
606
|
+
detail: {
|
|
607
|
+
candidateReplacement: match.to,
|
|
608
|
+
expectedTarget: `Review identifier "${match.from}" before replacing "${options.from}" with "${options.to}".`,
|
|
609
|
+
judgment: 'unresolved',
|
|
610
|
+
matchedForm: match.from,
|
|
611
|
+
nodeKind: node.type,
|
|
612
|
+
preserveCautions: [caution],
|
|
613
|
+
reason: 'ast-identifier-target-segment-present',
|
|
614
|
+
signals: ['ast:identifier-rename'],
|
|
615
|
+
span: {
|
|
616
|
+
column: location.column,
|
|
617
|
+
end: span.end,
|
|
618
|
+
line: location.line,
|
|
619
|
+
start: span.start,
|
|
620
|
+
},
|
|
621
|
+
suggestedValidation: 'bun run typecheck',
|
|
622
|
+
symbol: match.from,
|
|
623
|
+
},
|
|
624
|
+
kind: 'review',
|
|
625
|
+
note: caution,
|
|
626
|
+
reason: 'ast-identifier-target-segment-present',
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const declaration = context.getDeclaration(match.from);
|
|
335
631
|
if (declaration && reviewDeclarationTypes.has(declaration.type)) {
|
|
336
632
|
const location = offsetToLineColumn(context.source, span.start);
|
|
337
|
-
const caution = `Identifier "${
|
|
633
|
+
const caution = `Identifier "${match.from}" resolves to ${declaration.type}; routed to review.`;
|
|
338
634
|
return {
|
|
339
635
|
detail: {
|
|
340
|
-
candidateReplacement:
|
|
341
|
-
expectedTarget: `Rename identifier "${
|
|
636
|
+
candidateReplacement: match.to,
|
|
637
|
+
expectedTarget: `Rename identifier "${match.from}" to "${match.to}".`,
|
|
342
638
|
judgment: 'unresolved',
|
|
343
|
-
matchedForm:
|
|
639
|
+
matchedForm: match.from,
|
|
344
640
|
nodeKind: node.type,
|
|
345
641
|
preserveCautions: [caution],
|
|
346
642
|
reason: 'ast-identifier-review-declaration',
|
|
@@ -352,7 +648,7 @@ export const createAstIdentifierRenameClass = (
|
|
|
352
648
|
start: span.start,
|
|
353
649
|
},
|
|
354
650
|
suggestedValidation: 'bun run typecheck',
|
|
355
|
-
symbol:
|
|
651
|
+
symbol: match.from,
|
|
356
652
|
},
|
|
357
653
|
kind: 'review',
|
|
358
654
|
note: caution,
|
|
@@ -360,10 +656,43 @@ export const createAstIdentifierRenameClass = (
|
|
|
360
656
|
};
|
|
361
657
|
}
|
|
362
658
|
|
|
659
|
+
if (
|
|
660
|
+
context.parent?.type === 'ImportSpecifier' ||
|
|
661
|
+
context.parent?.type === 'ImportDefaultSpecifier' ||
|
|
662
|
+
context.parent?.type === 'ImportNamespaceSpecifier' ||
|
|
663
|
+
context.parent?.type === 'ExportSpecifier'
|
|
664
|
+
) {
|
|
665
|
+
const location = offsetToLineColumn(context.source, span.start);
|
|
666
|
+
const caution = `Identifier "${match.from}" names an import or export boundary; routed to review.`;
|
|
667
|
+
return {
|
|
668
|
+
detail: {
|
|
669
|
+
candidateReplacement: match.to,
|
|
670
|
+
expectedTarget: `Review public or foreign identifier "${match.from}" before renaming it to "${match.to}".`,
|
|
671
|
+
judgment: 'unresolved',
|
|
672
|
+
matchedForm: match.from,
|
|
673
|
+
nodeKind: node.type,
|
|
674
|
+
preserveCautions: [caution],
|
|
675
|
+
reason: 'ast-identifier-module-boundary',
|
|
676
|
+
signals: ['ast:identifier-rename'],
|
|
677
|
+
span: {
|
|
678
|
+
column: location.column,
|
|
679
|
+
end: span.end,
|
|
680
|
+
line: location.line,
|
|
681
|
+
start: span.start,
|
|
682
|
+
},
|
|
683
|
+
suggestedValidation: 'bun run typecheck',
|
|
684
|
+
symbol: match.from,
|
|
685
|
+
},
|
|
686
|
+
kind: 'review',
|
|
687
|
+
note: caution,
|
|
688
|
+
reason: 'ast-identifier-module-boundary',
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
363
692
|
return {
|
|
364
|
-
edit: createSourceEdit(span.start, span.end,
|
|
693
|
+
edit: createSourceEdit(span.start, span.end, match.to),
|
|
365
694
|
kind: 'edit',
|
|
366
|
-
note: `Renamed identifier "${
|
|
695
|
+
note: `Renamed identifier "${match.from}" to "${match.to}".`,
|
|
367
696
|
};
|
|
368
697
|
},
|
|
369
698
|
});
|
|
@@ -383,6 +712,35 @@ const stringLiteralValueSpan = (
|
|
|
383
712
|
return { end: start + value.length, start };
|
|
384
713
|
};
|
|
385
714
|
|
|
715
|
+
const isAdjacentModuleRoute = (value: string, route: string): boolean =>
|
|
716
|
+
value.startsWith(`${route}/`) || value.startsWith(`${route}.`);
|
|
717
|
+
|
|
718
|
+
interface AstStringLiteralMatch {
|
|
719
|
+
readonly adjacentModuleRoute: boolean;
|
|
720
|
+
readonly value: string;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const matchAstStringLiteral = (
|
|
724
|
+
node: AstNode,
|
|
725
|
+
options: AstStringLiteralRenameClassOptions
|
|
726
|
+
): AstStringLiteralMatch | null => {
|
|
727
|
+
if (!isStringLiteral(node) && node.type !== 'TemplateLiteral') {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
const value = isStringLiteral(node)
|
|
731
|
+
? getStringValue(node)
|
|
732
|
+
: extractPlainTemplateLiteral(node);
|
|
733
|
+
if (value === null) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const adjacentModuleRoute =
|
|
737
|
+
options.allowModuleSpecifier === true &&
|
|
738
|
+
isAdjacentModuleRoute(value, options.from);
|
|
739
|
+
return value === options.from || adjacentModuleRoute
|
|
740
|
+
? { adjacentModuleRoute, value }
|
|
741
|
+
: null;
|
|
742
|
+
};
|
|
743
|
+
|
|
386
744
|
export const createAstStringLiteralRenameClass = (
|
|
387
745
|
options: AstStringLiteralRenameClassOptions
|
|
388
746
|
): RegradeClass =>
|
|
@@ -393,11 +751,13 @@ export const createAstStringLiteralRenameClass = (
|
|
|
393
751
|
id:
|
|
394
752
|
options.id ?? `ast-string-literal-rename:${options.from}->${options.to}`,
|
|
395
753
|
visit: (node, context) => {
|
|
396
|
-
|
|
754
|
+
const match = matchAstStringLiteral(node, options);
|
|
755
|
+
if (match === null) {
|
|
397
756
|
return null;
|
|
398
757
|
}
|
|
758
|
+
const { adjacentModuleRoute, value } = match;
|
|
399
759
|
|
|
400
|
-
const span = stringLiteralValueSpan(node, context.source,
|
|
760
|
+
const span = stringLiteralValueSpan(node, context.source, value);
|
|
401
761
|
if (span === null) {
|
|
402
762
|
const location = offsetToLineColumn(context.source, node.start);
|
|
403
763
|
const caution = `String literal "${options.from}" token span could not be verified; routed to review.`;
|
|
@@ -429,7 +789,7 @@ export const createAstStringLiteralRenameClass = (
|
|
|
429
789
|
if (
|
|
430
790
|
options.shouldPreserve?.({
|
|
431
791
|
end: span.end,
|
|
432
|
-
from:
|
|
792
|
+
from: value,
|
|
433
793
|
path: context.path,
|
|
434
794
|
source: context.source,
|
|
435
795
|
start: span.start,
|
|
@@ -439,6 +799,139 @@ export const createAstStringLiteralRenameClass = (
|
|
|
439
799
|
return null;
|
|
440
800
|
}
|
|
441
801
|
|
|
802
|
+
if (adjacentModuleRoute) {
|
|
803
|
+
const location = offsetToLineColumn(context.source, node.start);
|
|
804
|
+
const caution = `Module route "${value}" is adjacent to governed route "${options.from}"; routed to review.`;
|
|
805
|
+
return {
|
|
806
|
+
detail: {
|
|
807
|
+
candidateReplacement: `${options.to}${value.slice(options.from.length)}`,
|
|
808
|
+
expectedTarget: `Review whether module route "${value}" moves with governed route "${options.from}".`,
|
|
809
|
+
judgment: 'unresolved',
|
|
810
|
+
matchedForm: value,
|
|
811
|
+
nodeKind: node.type,
|
|
812
|
+
preserveCautions: [caution],
|
|
813
|
+
reason: 'ast-string-literal-adjacent-module-route',
|
|
814
|
+
signals: ['ast:string-literal-rename', 'ast:module-specifier'],
|
|
815
|
+
span: {
|
|
816
|
+
column: location.column,
|
|
817
|
+
end: node.end,
|
|
818
|
+
line: location.line,
|
|
819
|
+
start: node.start,
|
|
820
|
+
},
|
|
821
|
+
suggestedValidation:
|
|
822
|
+
'Confirm whether the adjacent module route is public, private, or intentionally preserved.',
|
|
823
|
+
symbol: value,
|
|
824
|
+
},
|
|
825
|
+
kind: 'review',
|
|
826
|
+
note: caution,
|
|
827
|
+
reason: 'ast-string-literal-adjacent-module-route',
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (
|
|
832
|
+
options.allowModuleSpecifier !== true &&
|
|
833
|
+
isModuleSpecifierPosition(context)
|
|
834
|
+
) {
|
|
835
|
+
const location = offsetToLineColumn(context.source, node.start);
|
|
836
|
+
const caution = `String literal "${options.from}" is a module specifier; routed to review.`;
|
|
837
|
+
return {
|
|
838
|
+
detail: {
|
|
839
|
+
candidateReplacement: options.to,
|
|
840
|
+
expectedTarget: `Review module route "${options.from}" before renaming it to "${options.to}".`,
|
|
841
|
+
judgment: 'unresolved',
|
|
842
|
+
matchedForm: options.from,
|
|
843
|
+
nodeKind: node.type,
|
|
844
|
+
preserveCautions: [caution],
|
|
845
|
+
reason: 'ast-string-literal-module-specifier',
|
|
846
|
+
signals: ['ast:string-literal-rename', 'ast:module-specifier'],
|
|
847
|
+
span: {
|
|
848
|
+
column: location.column,
|
|
849
|
+
end: node.end,
|
|
850
|
+
line: location.line,
|
|
851
|
+
start: node.start,
|
|
852
|
+
},
|
|
853
|
+
suggestedValidation:
|
|
854
|
+
'Confirm the module route is owned by this transition before changing it.',
|
|
855
|
+
symbol: options.from,
|
|
856
|
+
},
|
|
857
|
+
kind: 'review',
|
|
858
|
+
note: caution,
|
|
859
|
+
reason: 'ast-string-literal-module-specifier',
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (isTargetPackageMissing(context, options.targetPackage)) {
|
|
864
|
+
const location = offsetToLineColumn(context.source, node.start);
|
|
865
|
+
const manifest = context.package?.path ?? 'the owning package.json';
|
|
866
|
+
const invalidManifest = context.package?.manifestState === 'invalid';
|
|
867
|
+
const caution = invalidManifest
|
|
868
|
+
? `Package manifest ${manifest} is invalid; routed to review before rewriting package route "${options.to}".`
|
|
869
|
+
: `Package route "${options.to}" is not declared by ${manifest}; routed to review before rewriting source.`;
|
|
870
|
+
return {
|
|
871
|
+
detail: {
|
|
872
|
+
candidateReplacement: options.to,
|
|
873
|
+
expectedTarget: invalidManifest
|
|
874
|
+
? `Fix invalid package manifest ${manifest}, declare "${options.targetPackage}", install dependencies, then rename "${options.from}" to "${options.to}".`
|
|
875
|
+
: `Declare "${options.targetPackage}" in ${manifest}, install dependencies, then rename "${options.from}" to "${options.to}".`,
|
|
876
|
+
judgment: 'unresolved',
|
|
877
|
+
matchedForm: options.from,
|
|
878
|
+
nodeKind: node.type,
|
|
879
|
+
preserveCautions: [caution],
|
|
880
|
+
reason: 'package-route-target-dependency-unverified',
|
|
881
|
+
signals: [
|
|
882
|
+
'ast:string-literal-rename',
|
|
883
|
+
'package:dependency',
|
|
884
|
+
...(invalidManifest ? ['package:manifest-invalid'] : []),
|
|
885
|
+
],
|
|
886
|
+
span: {
|
|
887
|
+
column: location.column,
|
|
888
|
+
end: node.end,
|
|
889
|
+
line: location.line,
|
|
890
|
+
start: node.start,
|
|
891
|
+
},
|
|
892
|
+
suggestedValidation: invalidManifest
|
|
893
|
+
? 'Repair the package manifest, install dependencies, and run the package typecheck.'
|
|
894
|
+
: 'Install dependencies and run the package typecheck.',
|
|
895
|
+
symbol: options.from,
|
|
896
|
+
},
|
|
897
|
+
kind: 'review',
|
|
898
|
+
note: caution,
|
|
899
|
+
reason: 'package-route-target-dependency-unverified',
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (stringLiteralNeedsReview(options.match, context.key)) {
|
|
904
|
+
const location = offsetToLineColumn(context.source, node.start);
|
|
905
|
+
const caution = `String literal "${options.from}" is not proven framework-owned; routed to review.`;
|
|
906
|
+
return {
|
|
907
|
+
detail: {
|
|
908
|
+
candidateReplacement: options.to,
|
|
909
|
+
expectedTarget: `Review whether string literal "${options.from}" is framework-owned before renaming it to "${options.to}".`,
|
|
910
|
+
judgment: 'unresolved',
|
|
911
|
+
matchedForm: options.from,
|
|
912
|
+
nodeKind: node.type,
|
|
913
|
+
preserveCautions: [caution],
|
|
914
|
+
reason: 'ast-string-literal-review-position',
|
|
915
|
+
signals: [
|
|
916
|
+
'ast:string-literal-rename',
|
|
917
|
+
'ast:framework-position-required',
|
|
918
|
+
],
|
|
919
|
+
span: {
|
|
920
|
+
column: location.column,
|
|
921
|
+
end: node.end,
|
|
922
|
+
line: location.line,
|
|
923
|
+
start: node.start,
|
|
924
|
+
},
|
|
925
|
+
suggestedValidation:
|
|
926
|
+
'Confirm the literal is owned by a Trails contract before changing it.',
|
|
927
|
+
symbol: options.from,
|
|
928
|
+
},
|
|
929
|
+
kind: 'review',
|
|
930
|
+
note: caution,
|
|
931
|
+
reason: 'ast-string-literal-review-position',
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
442
935
|
return {
|
|
443
936
|
edit: createSourceEdit(span.start, span.end, options.to),
|
|
444
937
|
kind: 'edit',
|
|
@@ -454,28 +947,48 @@ export const createGovernedAstIdentifierRenameClasses = (
|
|
|
454
947
|
occurrence: AstIdentifierRenameOccurrence
|
|
455
948
|
) => boolean;
|
|
456
949
|
} = {}
|
|
457
|
-
): readonly RegradeClass[] =>
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
950
|
+
): readonly RegradeClass[] => {
|
|
951
|
+
const targetSegments = [
|
|
952
|
+
...new Set(transition.symbolRenames.map((rename) => rename.to)),
|
|
953
|
+
];
|
|
954
|
+
const orderedLiteralRenames = [...transition.stringLiteralRenames].toSorted(
|
|
955
|
+
(left, right) =>
|
|
956
|
+
right.from.length - left.from.length ||
|
|
957
|
+
left.from.localeCompare(right.from)
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
return [
|
|
961
|
+
...transition.symbolRenames.map((rename) =>
|
|
962
|
+
createAstIdentifierRenameClass({
|
|
963
|
+
describe: `Rename governed symbol "${rename.from}" to "${rename.to}" for ${transition.id}.`,
|
|
964
|
+
from: rename.from,
|
|
965
|
+
id: `ast-symbol-rename:${transition.id}:${rename.from}->${rename.to}`,
|
|
966
|
+
match: rename.match,
|
|
967
|
+
reviewDeclarationTypes: new Set(rename.reviewDeclarationTypes),
|
|
968
|
+
reviewExistingTargetSegments: targetSegments,
|
|
969
|
+
...(options.shouldPreserve === undefined
|
|
970
|
+
? {}
|
|
971
|
+
: { shouldPreserve: options.shouldPreserve }),
|
|
972
|
+
to: rename.to,
|
|
973
|
+
})
|
|
974
|
+
),
|
|
975
|
+
...orderedLiteralRenames.map((rename) =>
|
|
976
|
+
createAstStringLiteralRenameClass({
|
|
977
|
+
describe: `Rename governed string literal "${rename.from}" to "${rename.to}" for ${transition.id}.`,
|
|
978
|
+
from: rename.from,
|
|
979
|
+
id: `ast-string-literal-rename:${transition.id}:${rename.from}->${rename.to}`,
|
|
980
|
+
...(rename.match === undefined ? {} : { match: rename.match }),
|
|
981
|
+
...(options.shouldPreserve === undefined
|
|
982
|
+
? {}
|
|
983
|
+
: { shouldPreserve: options.shouldPreserve }),
|
|
984
|
+
...(rename.moduleSpecifier === undefined
|
|
985
|
+
? {}
|
|
986
|
+
: {
|
|
987
|
+
allowModuleSpecifier: true,
|
|
988
|
+
targetPackage: rename.moduleSpecifier.targetPackage,
|
|
989
|
+
}),
|
|
990
|
+
to: rename.to,
|
|
991
|
+
})
|
|
992
|
+
),
|
|
993
|
+
];
|
|
994
|
+
};
|