@ontrails/warden 1.0.0-beta.42 → 1.0.0-beta.45
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 +69 -0
- package/package.json +15 -10
- package/src/adapter-check.ts +1 -1
- package/src/cli.ts +81 -5
- package/src/index.ts +17 -4
- package/src/regrade-history.ts +599 -0
- package/src/rules/cli-command-route-coherence.ts +6 -6
- package/src/rules/draft-visible-debt.ts +1 -1
- package/src/rules/duplicate-exported-symbol.ts +4 -43
- package/src/rules/governed-symbol-residue.ts +146 -53
- package/src/rules/governed-vocabulary-permutation-watch.ts +76 -0
- package/src/rules/implementation-returns-result.ts +99 -9
- package/src/rules/index.ts +17 -6
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/{library-projection-coherence.ts → library-render-coherence.ts} +11 -14
- package/src/rules/metadata.ts +51 -14
- package/src/rules/no-redundant-result-error-wrap.ts +15 -17
- package/src/rules/no-retired-cross-vocabulary.ts +0 -1
- package/src/rules/{owner-projection-parity.ts → owner-render-parity.ts} +18 -18
- package/src/rules/public-internal-deep-imports.ts +1 -3
- package/src/rules/public-output-schema.ts +1 -1
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/retired-vocabulary.ts +775 -41
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/trail-versioning-source.ts +1 -1
- package/src/rules/trailhead-override-divergence.ts +4 -4
- package/src/rules/types.ts +51 -5
- package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
- package/src/trails/index.ts +3 -2
- package/src/trails/layer-field-name-drift.trail.ts +1 -1
- package/src/trails/{library-projection-coherence.trail.ts → library-render-coherence.trail.ts} +6 -6
- package/src/trails/{owner-projection-parity.trail.ts → owner-render-parity.trail.ts} +4 -4
- package/src/trails/public-output-schema.trail.ts +1 -1
- package/src/trails/run.ts +44 -2
- package/src/trails/schema.ts +41 -0
- package/src/trails/wrap-rule.ts +44 -2
|
@@ -12,7 +12,13 @@ import type {
|
|
|
12
12
|
GovernedVocabularySymbolRename,
|
|
13
13
|
GovernedVocabularyTransition,
|
|
14
14
|
} from './retired-vocabulary.js';
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
GovernedVocabularyHistoryEvidence,
|
|
17
|
+
ProjectAwareWardenRule,
|
|
18
|
+
ProjectContext,
|
|
19
|
+
WardenDiagnostic,
|
|
20
|
+
WardenFix,
|
|
21
|
+
} from './types.js';
|
|
16
22
|
|
|
17
23
|
const RULE_NAME = 'governed-symbol-residue';
|
|
18
24
|
|
|
@@ -37,11 +43,15 @@ const isAllowedSourcePath = (filePath: string): boolean =>
|
|
|
37
43
|
);
|
|
38
44
|
|
|
39
45
|
interface ActiveSymbolRename {
|
|
46
|
+
readonly history?: GovernedVocabularyHistoryEvidence;
|
|
40
47
|
readonly rename: GovernedVocabularySymbolRename;
|
|
41
48
|
readonly targetSegments: readonly string[];
|
|
49
|
+
readonly transition: GovernedVocabularyTransition;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
const activeSymbolRenames = (
|
|
52
|
+
const activeSymbolRenames = (
|
|
53
|
+
context?: ProjectContext
|
|
54
|
+
): readonly ActiveSymbolRename[] =>
|
|
45
55
|
listGovernedVocabularyTransitions()
|
|
46
56
|
.filter(
|
|
47
57
|
(transition) =>
|
|
@@ -52,9 +62,14 @@ const activeSymbolRenames = (): readonly ActiveSymbolRename[] =>
|
|
|
52
62
|
const targetSegments = [
|
|
53
63
|
...new Set(transition.symbolRenames.map((rename) => rename.to)),
|
|
54
64
|
];
|
|
65
|
+
const history = context?.governedVocabularyHistoryByTransitionId?.get(
|
|
66
|
+
transition.id
|
|
67
|
+
);
|
|
55
68
|
return transition.symbolRenames.map((rename) => ({
|
|
69
|
+
...(history === undefined ? {} : { history }),
|
|
56
70
|
rename,
|
|
57
71
|
targetSegments,
|
|
72
|
+
transition,
|
|
58
73
|
}));
|
|
59
74
|
});
|
|
60
75
|
|
|
@@ -353,12 +368,28 @@ const safeFixFor = (match: SymbolRenameMatch): WardenFix | undefined => {
|
|
|
353
368
|
};
|
|
354
369
|
};
|
|
355
370
|
|
|
371
|
+
const diagnosticMessageFor = (
|
|
372
|
+
match: SymbolRenameMatch,
|
|
373
|
+
transition?: GovernedVocabularyTransition,
|
|
374
|
+
history?: GovernedVocabularyHistoryEvidence
|
|
375
|
+
): string => {
|
|
376
|
+
if (history !== undefined) {
|
|
377
|
+
return `Retired governed symbol '${match.from}' was reintroduced after governed transition '${history.transitionId}' in '${history.path}' (${history.id}); migrate to '${match.to}'.`;
|
|
378
|
+
}
|
|
379
|
+
if (transition?.provenance.mode === 'regrade-history') {
|
|
380
|
+
return `Retired governed symbol '${match.from}' belongs to governed transition '${transition.id}', but no committed Regrade history proves the migration ran.`;
|
|
381
|
+
}
|
|
382
|
+
return `Retired governed symbol '${match.from}' should migrate to '${match.to}'.`;
|
|
383
|
+
};
|
|
384
|
+
|
|
356
385
|
const diagnosticFor = (
|
|
357
386
|
sourceCode: string,
|
|
358
387
|
filePath: string,
|
|
359
388
|
node: AstNode,
|
|
360
389
|
match: SymbolRenameMatch,
|
|
361
|
-
reviewReason: string | null
|
|
390
|
+
reviewReason: string | null,
|
|
391
|
+
transition?: GovernedVocabularyTransition,
|
|
392
|
+
history?: GovernedVocabularyHistoryEvidence
|
|
362
393
|
): WardenDiagnostic => {
|
|
363
394
|
const fix =
|
|
364
395
|
reviewReason === null
|
|
@@ -368,71 +399,133 @@ const diagnosticFor = (
|
|
|
368
399
|
filePath,
|
|
369
400
|
...(fix === undefined ? {} : { fix }),
|
|
370
401
|
line: offsetToLine(sourceCode, node.start),
|
|
371
|
-
message:
|
|
402
|
+
message: diagnosticMessageFor(match, transition, history),
|
|
372
403
|
rule: RULE_NAME,
|
|
373
404
|
severity: 'error',
|
|
374
405
|
};
|
|
375
406
|
};
|
|
376
407
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
408
|
+
const provenanceDiagnostics = (
|
|
409
|
+
context: ProjectContext
|
|
410
|
+
): readonly WardenDiagnostic[] => {
|
|
411
|
+
const historyIssues = context.governedVocabularyHistoryIssues ?? [];
|
|
412
|
+
const invalidTransitionIds = new Set(
|
|
413
|
+
historyIssues.flatMap((issue) =>
|
|
414
|
+
issue.transitionId === undefined ? [] : [issue.transitionId]
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
const issues = historyIssues.map((issue) => ({
|
|
418
|
+
filePath: issue.path,
|
|
419
|
+
line: 1,
|
|
420
|
+
message: issue.message,
|
|
421
|
+
rule: RULE_NAME,
|
|
422
|
+
severity: 'error' as const,
|
|
423
|
+
}));
|
|
424
|
+
const missing = listGovernedVocabularyTransitions()
|
|
425
|
+
.filter(
|
|
426
|
+
(transition) =>
|
|
427
|
+
context.governedVocabularyHistoryRequired === true &&
|
|
428
|
+
transition.status === 'complete' &&
|
|
429
|
+
transition.provenance.mode === 'regrade-history' &&
|
|
430
|
+
!invalidTransitionIds.has(transition.id) &&
|
|
431
|
+
!context.governedVocabularyHistoryByTransitionId?.has(transition.id)
|
|
432
|
+
)
|
|
433
|
+
.map((transition) => ({
|
|
434
|
+
filePath: '<governed-vocabulary-history>',
|
|
435
|
+
line: 1,
|
|
436
|
+
message: `Governed transition '${transition.id}' is complete but has no valid committed Regrade history provenance. Run Regrade as the primary migration path before manual review or cleanup.`,
|
|
437
|
+
rule: RULE_NAME,
|
|
438
|
+
severity: 'error' as const,
|
|
439
|
+
}));
|
|
440
|
+
return [...issues, ...missing];
|
|
441
|
+
};
|
|
382
442
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
443
|
+
const checkSource = (
|
|
444
|
+
sourceCode: string,
|
|
445
|
+
filePath: string,
|
|
446
|
+
projectContext?: ProjectContext
|
|
447
|
+
): readonly WardenDiagnostic[] => {
|
|
448
|
+
if (isAllowedSourcePath(filePath)) {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
387
451
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
452
|
+
const renames = activeSymbolRenames(projectContext);
|
|
453
|
+
if (renames.length === 0) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
392
456
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
const name = identifierName(node);
|
|
399
|
-
if (name !== null) {
|
|
400
|
-
shorthandIdentifiers.add(name);
|
|
401
|
-
}
|
|
402
|
-
});
|
|
457
|
+
const parsed = parseWithDiagnostics(filePath, sourceCode);
|
|
458
|
+
if (!parsed.ast || parsed.diagnostics.length > 0) {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
403
461
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
462
|
+
const shorthandIdentifiers = new Set<string>();
|
|
463
|
+
walkWithScopeContext(parsed.ast, (node, astContext) => {
|
|
464
|
+
if (!isShorthandPropertyKeyVisit(node, astContext)) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const name = identifierName(node);
|
|
468
|
+
if (name !== null) {
|
|
469
|
+
shorthandIdentifiers.add(name);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
409
472
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.find((candidate) => candidate.match !== null);
|
|
416
|
-
if (activeMatch === undefined || activeMatch.match === null) {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const { match, targetSegments } = activeMatch;
|
|
473
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
474
|
+
walkWithScopeContext(parsed.ast, (node, astContext) => {
|
|
475
|
+
if (isDuplicateShorthandValueVisit(node, astContext)) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
420
478
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
479
|
+
const activeMatch = renames
|
|
480
|
+
.map(({ history, rename, targetSegments, transition }) => ({
|
|
481
|
+
history,
|
|
482
|
+
match: resolveSymbolRenameMatch(node, sourceCode, rename),
|
|
483
|
+
targetSegments,
|
|
484
|
+
transition,
|
|
485
|
+
}))
|
|
486
|
+
.find((candidate) => candidate.match !== null);
|
|
487
|
+
if (activeMatch === undefined || activeMatch.match === null) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const { history, match, targetSegments, transition } = activeMatch;
|
|
491
|
+
|
|
492
|
+
diagnostics.push(
|
|
493
|
+
diagnosticFor(
|
|
494
|
+
sourceCode,
|
|
495
|
+
filePath,
|
|
496
|
+
node,
|
|
497
|
+
match,
|
|
498
|
+
reviewReasonFor(
|
|
499
|
+
astContext,
|
|
426
500
|
match,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
501
|
+
shorthandIdentifiers,
|
|
502
|
+
targetSegments
|
|
503
|
+
),
|
|
504
|
+
transition,
|
|
505
|
+
history
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
});
|
|
431
509
|
|
|
432
|
-
|
|
510
|
+
return diagnostics;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
export const governedSymbolResidue: ProjectAwareWardenRule = {
|
|
514
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
515
|
+
return checkSource(sourceCode, filePath);
|
|
516
|
+
},
|
|
517
|
+
checkProject(context: ProjectContext): readonly WardenDiagnostic[] {
|
|
518
|
+
return provenanceDiagnostics(context);
|
|
519
|
+
},
|
|
520
|
+
checkWithContext(
|
|
521
|
+
sourceCode: string,
|
|
522
|
+
filePath: string,
|
|
523
|
+
context: ProjectContext
|
|
524
|
+
): readonly WardenDiagnostic[] {
|
|
525
|
+
return checkSource(sourceCode, filePath, context);
|
|
433
526
|
},
|
|
434
527
|
description:
|
|
435
|
-
'
|
|
528
|
+
'Require committed Regrade provenance for governed transitions and detect retired symbols that remain or return.',
|
|
436
529
|
name: RULE_NAME,
|
|
437
530
|
severity: 'error',
|
|
438
531
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GovernedVocabularyHistoryEvidence,
|
|
3
|
+
GovernedVocabularyHistoryFormObservation,
|
|
4
|
+
ProjectAwareWardenRule,
|
|
5
|
+
ProjectContext,
|
|
6
|
+
WardenDiagnostic,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
const RULE_NAME = 'governed-vocabulary-permutation-watch';
|
|
10
|
+
|
|
11
|
+
const formIdentity = (
|
|
12
|
+
form: string,
|
|
13
|
+
evidence: GovernedVocabularyHistoryEvidence
|
|
14
|
+
): string => (evidence.caseSensitive ? form : form.toLowerCase());
|
|
15
|
+
|
|
16
|
+
const isUnknownPermutation = (
|
|
17
|
+
observation: GovernedVocabularyHistoryFormObservation
|
|
18
|
+
): boolean =>
|
|
19
|
+
observation.reason === 'unclassified-neighbor' &&
|
|
20
|
+
observation.verdict === 'deferred' &&
|
|
21
|
+
(observation.scopeTier === undefined || observation.scopeTier === 'in-scope');
|
|
22
|
+
|
|
23
|
+
const compareObservation = (
|
|
24
|
+
left: GovernedVocabularyHistoryFormObservation,
|
|
25
|
+
right: GovernedVocabularyHistoryFormObservation
|
|
26
|
+
): number =>
|
|
27
|
+
left.path.localeCompare(right.path) ||
|
|
28
|
+
left.line - right.line ||
|
|
29
|
+
left.form.localeCompare(right.form);
|
|
30
|
+
|
|
31
|
+
const diagnosticsForHistory = (
|
|
32
|
+
evidence: GovernedVocabularyHistoryEvidence
|
|
33
|
+
): readonly WardenDiagnostic[] => {
|
|
34
|
+
const unknownByIdentity = new Map<
|
|
35
|
+
string,
|
|
36
|
+
GovernedVocabularyHistoryFormObservation
|
|
37
|
+
>();
|
|
38
|
+
|
|
39
|
+
for (const observation of evidence.latestFormObservations) {
|
|
40
|
+
if (!isUnknownPermutation(observation)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const identity = formIdentity(observation.form, evidence);
|
|
44
|
+
const current = unknownByIdentity.get(identity);
|
|
45
|
+
if (current === undefined || compareObservation(observation, current) < 0) {
|
|
46
|
+
unknownByIdentity.set(identity, observation);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return [...unknownByIdentity.entries()]
|
|
51
|
+
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
52
|
+
.map(([, observation]) => ({
|
|
53
|
+
filePath: observation.path,
|
|
54
|
+
line: observation.line,
|
|
55
|
+
message: `Governed transition '${evidence.transitionId}' recorded unknown vocabulary form '${observation.form}' in committed Regrade history '${evidence.path}' (${evidence.id}). Add the form and run an incremental plan, or classify it as out-of-family or preserved.`,
|
|
56
|
+
rule: RULE_NAME,
|
|
57
|
+
severity: 'warn' as const,
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const checkProject = (context: ProjectContext): readonly WardenDiagnostic[] =>
|
|
62
|
+
[...(context.governedVocabularyHistoryByTransitionId?.values() ?? [])]
|
|
63
|
+
.toSorted((left, right) =>
|
|
64
|
+
left.transitionId.localeCompare(right.transitionId)
|
|
65
|
+
)
|
|
66
|
+
.flatMap(diagnosticsForHistory);
|
|
67
|
+
|
|
68
|
+
export const governedVocabularyPermutationWatch: ProjectAwareWardenRule = {
|
|
69
|
+
check: () => [],
|
|
70
|
+
checkProject,
|
|
71
|
+
checkWithContext: () => [],
|
|
72
|
+
description:
|
|
73
|
+
'Advise when committed Regrade history records unclassified permutations of governed vocabulary.',
|
|
74
|
+
name: RULE_NAME,
|
|
75
|
+
severity: 'warn',
|
|
76
|
+
};
|
|
@@ -25,8 +25,11 @@ import {
|
|
|
25
25
|
getNodeImported,
|
|
26
26
|
getNodeInit,
|
|
27
27
|
getNodeLocal,
|
|
28
|
+
getNodeLeft,
|
|
28
29
|
getNodeName,
|
|
30
|
+
getNodeOperator,
|
|
29
31
|
getNodeReturnType,
|
|
32
|
+
getNodeRight,
|
|
30
33
|
getNodeSource,
|
|
31
34
|
getNodeTypeAnnotation,
|
|
32
35
|
getNodeValue,
|
|
@@ -97,12 +100,15 @@ export type ScopedHelperMap = ReadonlyMap<
|
|
|
97
100
|
|
|
98
101
|
export type MutableScopedHelperMap = Map<ReadonlySet<string>, Set<string>>;
|
|
99
102
|
|
|
100
|
-
type ScopedResultVariableMap = ReadonlyMap<
|
|
103
|
+
export type ScopedResultVariableMap = ReadonlyMap<
|
|
101
104
|
ReadonlySet<string>,
|
|
102
105
|
ReadonlySet<string>
|
|
103
106
|
>;
|
|
104
107
|
|
|
105
|
-
type MutableScopedResultVariableMap = Map<
|
|
108
|
+
export type MutableScopedResultVariableMap = Map<
|
|
109
|
+
ReadonlySet<string>,
|
|
110
|
+
Set<string>
|
|
111
|
+
>;
|
|
106
112
|
|
|
107
113
|
export const findNearestBindingScope = (
|
|
108
114
|
name: string,
|
|
@@ -224,8 +230,8 @@ const unwrapReturnExpression = (node: AstNode): AstNode => {
|
|
|
224
230
|
return current;
|
|
225
231
|
};
|
|
226
232
|
|
|
227
|
-
/** Check
|
|
228
|
-
const
|
|
233
|
+
/** Check whether an expression has visible Result-producing provenance. */
|
|
234
|
+
export const isResultProducingExpression = (
|
|
229
235
|
argument: AstNode,
|
|
230
236
|
helperNames: ReadonlySet<string>,
|
|
231
237
|
resultVars: ScopedResultVariableMap,
|
|
@@ -240,7 +246,7 @@ const isAllowedReturnArgument = (
|
|
|
240
246
|
return (
|
|
241
247
|
consequent !== undefined &&
|
|
242
248
|
alternate !== undefined &&
|
|
243
|
-
|
|
249
|
+
isResultProducingExpression(
|
|
244
250
|
consequent,
|
|
245
251
|
helperNames,
|
|
246
252
|
resultVars,
|
|
@@ -248,7 +254,7 @@ const isAllowedReturnArgument = (
|
|
|
248
254
|
scopes,
|
|
249
255
|
scopedHelpers
|
|
250
256
|
) &&
|
|
251
|
-
|
|
257
|
+
isResultProducingExpression(
|
|
252
258
|
alternate,
|
|
253
259
|
helperNames,
|
|
254
260
|
resultVars,
|
|
@@ -369,6 +375,14 @@ const addScopedResultVariable = (
|
|
|
369
375
|
resultVars.set(scope, new Set([name]));
|
|
370
376
|
};
|
|
371
377
|
|
|
378
|
+
const clearScopedResultVariable = (
|
|
379
|
+
resultVars: MutableScopedResultVariableMap,
|
|
380
|
+
scope: ReadonlySet<string>,
|
|
381
|
+
name: string
|
|
382
|
+
): void => {
|
|
383
|
+
resultVars.get(scope)?.delete(name);
|
|
384
|
+
};
|
|
385
|
+
|
|
372
386
|
/** Record `const helper = (): Result<...> => ...` declarations for the current lexical scope. */
|
|
373
387
|
export const trackScopedResultHelperDeclaration = (
|
|
374
388
|
node: AstNode,
|
|
@@ -440,8 +454,14 @@ const trackResultVariable = (
|
|
|
440
454
|
}
|
|
441
455
|
if (
|
|
442
456
|
hasResultVariableAnnotation(id, sourceCode, resultTypeNames) ||
|
|
443
|
-
|
|
444
|
-
|
|
457
|
+
isResultProducingExpression(
|
|
458
|
+
init,
|
|
459
|
+
helperNames,
|
|
460
|
+
resultVars,
|
|
461
|
+
namespaceHelpers,
|
|
462
|
+
scopes,
|
|
463
|
+
scopedHelpers
|
|
464
|
+
)
|
|
445
465
|
) {
|
|
446
466
|
const bindingScope = findNearestBindingScope(name, scopes);
|
|
447
467
|
if (bindingScope) {
|
|
@@ -451,6 +471,63 @@ const trackResultVariable = (
|
|
|
451
471
|
}
|
|
452
472
|
};
|
|
453
473
|
|
|
474
|
+
export const collectDirectResultAssignments = (
|
|
475
|
+
body: AstNode
|
|
476
|
+
): ReadonlySet<AstNode> => {
|
|
477
|
+
const assignments = new Set<AstNode>();
|
|
478
|
+
for (const statement of getNodeBodyStatements(body)) {
|
|
479
|
+
if (statement.type !== 'ExpressionStatement') {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const expression = getNodeExpression(statement);
|
|
483
|
+
if (expression?.type === 'AssignmentExpression') {
|
|
484
|
+
assignments.add(expression);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return assignments;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const trackResultAssignment = (
|
|
491
|
+
node: AstNode,
|
|
492
|
+
resultVars: MutableScopedResultVariableMap,
|
|
493
|
+
helperNames: ReadonlySet<string>,
|
|
494
|
+
namespaceHelpers: NamespaceHelperMap,
|
|
495
|
+
directAssignments: ReadonlySet<AstNode>,
|
|
496
|
+
scopes: readonly ReadonlySet<string>[],
|
|
497
|
+
scopedHelpers: ScopedHelperMap
|
|
498
|
+
): void => {
|
|
499
|
+
const left = getNodeLeft(node);
|
|
500
|
+
const name = identifierName(left);
|
|
501
|
+
if (!name) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const bindingScope = findNearestBindingScope(name, scopes);
|
|
505
|
+
if (!bindingScope) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const right = getNodeRight(node);
|
|
509
|
+
const resultRhs =
|
|
510
|
+
getNodeOperator(node) === '=' &&
|
|
511
|
+
right &&
|
|
512
|
+
isResultProducingExpression(
|
|
513
|
+
right,
|
|
514
|
+
helperNames,
|
|
515
|
+
resultVars,
|
|
516
|
+
namespaceHelpers,
|
|
517
|
+
scopes,
|
|
518
|
+
scopedHelpers
|
|
519
|
+
);
|
|
520
|
+
if (
|
|
521
|
+
resultRhs &&
|
|
522
|
+
(isScopedResultVariableBinding(name, scopes, resultVars) ||
|
|
523
|
+
directAssignments.has(node))
|
|
524
|
+
) {
|
|
525
|
+
addScopedResultVariable(resultVars, bindingScope, name);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
clearScopedResultVariable(resultVars, bindingScope, name);
|
|
529
|
+
};
|
|
530
|
+
|
|
454
531
|
// ---------------------------------------------------------------------------
|
|
455
532
|
// Return statement checking
|
|
456
533
|
// ---------------------------------------------------------------------------
|
|
@@ -470,6 +547,7 @@ const checkReturnStatements = (
|
|
|
470
547
|
const resultVars: MutableScopedResultVariableMap = new Map();
|
|
471
548
|
const scopedHelpers: MutableScopedHelperMap = new Map();
|
|
472
549
|
const initialScopes = implScope.size > 0 ? [implScope] : [];
|
|
550
|
+
const directAssignments = collectDirectResultAssignments(blockBody);
|
|
473
551
|
|
|
474
552
|
walkWithScopes(
|
|
475
553
|
blockBody,
|
|
@@ -494,6 +572,18 @@ const checkReturnStatements = (
|
|
|
494
572
|
);
|
|
495
573
|
}
|
|
496
574
|
|
|
575
|
+
if (node.type === 'AssignmentExpression') {
|
|
576
|
+
trackResultAssignment(
|
|
577
|
+
node,
|
|
578
|
+
resultVars,
|
|
579
|
+
helperNames,
|
|
580
|
+
namespaceHelpers,
|
|
581
|
+
directAssignments,
|
|
582
|
+
currentScopes,
|
|
583
|
+
scopedHelpers
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
497
587
|
if (node.type !== 'ReturnStatement') {
|
|
498
588
|
return;
|
|
499
589
|
}
|
|
@@ -505,7 +595,7 @@ const checkReturnStatements = (
|
|
|
505
595
|
}
|
|
506
596
|
|
|
507
597
|
if (
|
|
508
|
-
|
|
598
|
+
isResultProducingExpression(
|
|
509
599
|
argument,
|
|
510
600
|
helperNames,
|
|
511
601
|
resultVars,
|
package/src/rules/index.ts
CHANGED
|
@@ -15,12 +15,13 @@ import { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
|
15
15
|
import { exampleValid } from './example-valid.js';
|
|
16
16
|
import { firesDeclarations } from './fires-declarations.js';
|
|
17
17
|
import { governedSymbolResidue } from './governed-symbol-residue.js';
|
|
18
|
+
import { governedVocabularyPermutationWatch } from './governed-vocabulary-permutation-watch.js';
|
|
18
19
|
import { implementationReturnsResult } from './implementation-returns-result.js';
|
|
19
20
|
import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
|
|
20
21
|
import { incompleteCrud } from './incomplete-crud.js';
|
|
21
22
|
import { intentPropagation } from './intent-propagation.js';
|
|
22
23
|
import { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
23
|
-
import {
|
|
24
|
+
import { libraryRenderCoherence } from './library-render-coherence.js';
|
|
24
25
|
import { missingVisibility } from './missing-visibility.js';
|
|
25
26
|
import { missingReconcile } from './missing-reconcile.js';
|
|
26
27
|
import { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
@@ -37,7 +38,7 @@ import { noThrowInImplementation } from './no-throw-in-implementation.js';
|
|
|
37
38
|
import { noTopLevelSurface } from './no-top-level-surface.js';
|
|
38
39
|
import { onReferencesExist } from './on-references-exist.js';
|
|
39
40
|
import { orphanedSignal } from './orphaned-signal.js';
|
|
40
|
-
import {
|
|
41
|
+
import { ownerRenderParity } from './owner-render-parity.js';
|
|
41
42
|
import { permitGovernance } from './permit-governance.js';
|
|
42
43
|
import { preferSchemaInference } from './prefer-schema-inference.js';
|
|
43
44
|
import { publicExportExampleCoverage } from './public-export-example-coverage.js';
|
|
@@ -79,6 +80,9 @@ import { wardenRulesUseAst } from './warden-rules-use-ast.js';
|
|
|
79
80
|
import { webhookRouteCollision } from './webhook-route-collision.js';
|
|
80
81
|
|
|
81
82
|
export type {
|
|
83
|
+
GovernedVocabularyHistoryEvidence,
|
|
84
|
+
GovernedVocabularyHistoryFormObservation,
|
|
85
|
+
GovernedVocabularyHistoryIssue,
|
|
82
86
|
WardenFix,
|
|
83
87
|
WardenFixCapability,
|
|
84
88
|
WardenFixClass,
|
|
@@ -106,7 +110,10 @@ export {
|
|
|
106
110
|
formatGovernedVocabularyTransitionGuide,
|
|
107
111
|
getGovernedVocabularyTransition,
|
|
108
112
|
governedVocabularyLiteralRenameSchema,
|
|
113
|
+
governedVocabularyFileRenameSchema,
|
|
114
|
+
governedVocabularyHistoryProvenanceSchema,
|
|
109
115
|
governedVocabularyPreserveRuleSchema,
|
|
116
|
+
governedVocabularyProvenancePolicySchema,
|
|
110
117
|
governedVocabularyRegistrySchema,
|
|
111
118
|
governedVocabularyScopeSchema,
|
|
112
119
|
governedVocabularySymbolRenameSchema,
|
|
@@ -119,7 +126,9 @@ export {
|
|
|
119
126
|
} from './retired-vocabulary.js';
|
|
120
127
|
export type {
|
|
121
128
|
GovernedVocabularyLiteralRename,
|
|
129
|
+
GovernedVocabularyHistoryProvenance,
|
|
122
130
|
GovernedVocabularyPreserveRule,
|
|
131
|
+
GovernedVocabularyProvenancePolicy,
|
|
123
132
|
GovernedVocabularyScope,
|
|
124
133
|
GovernedVocabularySymbolRename,
|
|
125
134
|
GovernedVocabularyTarget,
|
|
@@ -157,10 +166,11 @@ export { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
|
157
166
|
export { exampleValid } from './example-valid.js';
|
|
158
167
|
export { firesDeclarations } from './fires-declarations.js';
|
|
159
168
|
export { governedSymbolResidue } from './governed-symbol-residue.js';
|
|
169
|
+
export { governedVocabularyPermutationWatch } from './governed-vocabulary-permutation-watch.js';
|
|
160
170
|
export { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
|
|
161
171
|
export { incompleteCrud } from './incomplete-crud.js';
|
|
162
172
|
export { intentPropagation } from './intent-propagation.js';
|
|
163
|
-
export {
|
|
173
|
+
export { libraryRenderCoherence } from './library-render-coherence.js';
|
|
164
174
|
export { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
165
175
|
export { missingVisibility } from './missing-visibility.js';
|
|
166
176
|
export { missingReconcile } from './missing-reconcile.js';
|
|
@@ -178,7 +188,7 @@ export { implementationReturnsResult } from './implementation-returns-result.js'
|
|
|
178
188
|
export { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
|
|
179
189
|
export { noTopLevelSurface } from './no-top-level-surface.js';
|
|
180
190
|
export { orphanedSignal } from './orphaned-signal.js';
|
|
181
|
-
export {
|
|
191
|
+
export { ownerRenderParity } from './owner-render-parity.js';
|
|
182
192
|
export { permitGovernance } from './permit-governance.js';
|
|
183
193
|
export { preferSchemaInference } from './prefer-schema-inference.js';
|
|
184
194
|
export { publicInternalDeepImports } from './public-internal-deep-imports.js';
|
|
@@ -235,6 +245,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
235
245
|
[exampleValid.name, exampleValid],
|
|
236
246
|
[firesDeclarations.name, firesDeclarations],
|
|
237
247
|
[governedSymbolResidue.name, governedSymbolResidue],
|
|
248
|
+
[governedVocabularyPermutationWatch.name, governedVocabularyPermutationWatch],
|
|
238
249
|
[incompleteCrud.name, incompleteCrud],
|
|
239
250
|
[intentPropagation.name, intentPropagation],
|
|
240
251
|
[layerFieldNameDrift.name, layerFieldNameDrift],
|
|
@@ -242,7 +253,7 @@ export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
|
|
|
242
253
|
[missingReconcile.name, missingReconcile],
|
|
243
254
|
[onReferencesExist.name, onReferencesExist],
|
|
244
255
|
[orphanedSignal.name, orphanedSignal],
|
|
245
|
-
[
|
|
256
|
+
[ownerRenderParity.name, ownerRenderParity],
|
|
246
257
|
[publicExportExampleCoverage.name, publicExportExampleCoverage],
|
|
247
258
|
[publicInternalDeepImports.name, publicInternalDeepImports],
|
|
248
259
|
[resourceDeclarations.name, resourceDeclarations],
|
|
@@ -297,7 +308,7 @@ export const wardenTopoRules: ReadonlyMap<string, TopoAwareWardenRule> =
|
|
|
297
308
|
[cliCommandRouteCoherence.name, cliCommandRouteCoherence],
|
|
298
309
|
[duplicatePublicContract.name, duplicatePublicContract],
|
|
299
310
|
[incompleteAccessorForStandardOp.name, incompleteAccessorForStandardOp],
|
|
300
|
-
[
|
|
311
|
+
[libraryRenderCoherence.name, libraryRenderCoherence],
|
|
301
312
|
[permitGovernance.name, permitGovernance],
|
|
302
313
|
[publicOutputSchema.name, publicOutputSchema],
|
|
303
314
|
[publicUnionOutputDiscriminants.name, publicUnionOutputDiscriminants],
|
|
@@ -60,7 +60,7 @@ const buildDiagnostic = (
|
|
|
60
60
|
): WardenDiagnostic => ({
|
|
61
61
|
filePath,
|
|
62
62
|
line: offsetToLine(sourceCode, node.start),
|
|
63
|
-
message: `layer-field-name-drift: surface-local reserved name set "${name}" can make layer input fields
|
|
63
|
+
message: `layer-field-name-drift: surface-local reserved name set "${name}" can make layer input fields render differently across surfaces. Import LAYER_FIELD_RESERVED_NAMES from @ontrails/core instead.`,
|
|
64
64
|
rule: RULE_NAME,
|
|
65
65
|
severity: 'error',
|
|
66
66
|
});
|
|
@@ -96,7 +96,7 @@ export const layerFieldNameDrift: WardenRule = {
|
|
|
96
96
|
return diagnostics;
|
|
97
97
|
},
|
|
98
98
|
description:
|
|
99
|
-
'Prevent surface-local reserved-name sets from drifting layer input field
|
|
99
|
+
'Prevent surface-local reserved-name sets from drifting layer input field rendering across surfaces.',
|
|
100
100
|
name: RULE_NAME,
|
|
101
101
|
severity: 'error',
|
|
102
102
|
};
|