@ontrails/warden 1.0.0-beta.4 → 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 +835 -13
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +27 -6
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +1570 -105
- package/src/command.ts +986 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +233 -23
- package/src/fix.ts +126 -0
- package/src/formatters.ts +78 -13
- package/src/guide.ts +245 -0
- package/src/index.ts +247 -15
- package/src/project-context.ts +446 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +79 -15
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +155 -0
- package/src/rules/draft-visible-debt.ts +82 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +438 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +583 -0
- package/src/rules/index.ts +277 -10
- package/src/rules/intent-propagation.ts +125 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +871 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +204 -0
- package/src/rules/no-sync-result-assumption.ts +1141 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +15 -8
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +149 -0
- package/src/rules/owner-projection-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +573 -0
- package/src/rules/public-internal-deep-imports.ts +456 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +167 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +633 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +653 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +354 -8
- package/src/rules/unmaterialized-activation-source.ts +85 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +89 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +160 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +237 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +224 -0
- package/src/workspaces.ts +199 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -218
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -161
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -15
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -34
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -47
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -129
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -227
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/ast.ts +0 -213
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildUserNamespaceContext,
|
|
3
|
+
collectEntityDefinitionIds,
|
|
4
|
+
collectImportAliasMap,
|
|
5
|
+
collectNamedEntityIds,
|
|
6
|
+
deriveEntityIdentifierName,
|
|
7
|
+
isUserNamespaceReceiverAllowed,
|
|
8
|
+
} from './source/entities.js';
|
|
9
|
+
import {
|
|
10
|
+
extractFirstStringArg,
|
|
11
|
+
findConfigProperty,
|
|
12
|
+
findTrailDefinitions,
|
|
13
|
+
getNodeCallee,
|
|
14
|
+
getNodeObject,
|
|
15
|
+
getNodeProperty,
|
|
16
|
+
identifierName,
|
|
17
|
+
isMemberAccessNonComputed,
|
|
18
|
+
offsetToLine,
|
|
19
|
+
parse,
|
|
20
|
+
} from '@ontrails/source';
|
|
21
|
+
import type { AstNode, TrailDefinition } from '@ontrails/source';
|
|
22
|
+
import type { UserNamespaceContext } from './source/entities.js';
|
|
23
|
+
import { mergeKnownEntityIds } from './entity-ids.js';
|
|
24
|
+
import { isTestFile } from './scan.js';
|
|
25
|
+
import type {
|
|
26
|
+
ProjectAwareWardenRule,
|
|
27
|
+
ProjectContext,
|
|
28
|
+
WardenDiagnostic,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
const isEntityCall = (node: AstNode): boolean =>
|
|
32
|
+
node.type === 'CallExpression' &&
|
|
33
|
+
identifierName(getNodeCallee(node)) === 'entity';
|
|
34
|
+
|
|
35
|
+
const getEntityElements = (config: AstNode): readonly AstNode[] => {
|
|
36
|
+
const entitiesProp = findConfigProperty(config, 'entities');
|
|
37
|
+
if (!entitiesProp) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const arrayNode = entitiesProp.value;
|
|
42
|
+
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const elements = (arrayNode as AstNode)['elements'] as
|
|
47
|
+
| readonly AstNode[]
|
|
48
|
+
| undefined;
|
|
49
|
+
return elements ?? [];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve `entities.user` to its entity name. When `userNamespace` carries a
|
|
54
|
+
* scope-aware `safeMemberStarts` set, the member access must appear in it —
|
|
55
|
+
* rejecting cases where `entities` is shadowed by a local binding such as a
|
|
56
|
+
* function parameter or `const entities = ...`. Without the set, falls back
|
|
57
|
+
* to the bare name check for backward compatibility.
|
|
58
|
+
*/
|
|
59
|
+
const resolveNamespaceMemberEntityName = (
|
|
60
|
+
element: AstNode,
|
|
61
|
+
userNamespace: UserNamespaceContext
|
|
62
|
+
): string | null => {
|
|
63
|
+
if (!isMemberAccessNonComputed(element)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const object = getNodeObject(element);
|
|
67
|
+
const property = getNodeProperty(element);
|
|
68
|
+
const receiver = object ? identifierName(object) : null;
|
|
69
|
+
if (
|
|
70
|
+
!receiver ||
|
|
71
|
+
!isUserNamespaceReceiverAllowed(receiver, element.start, userNamespace)
|
|
72
|
+
) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return property ? identifierName(property) : null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const resolveDeclaredEntityName = (
|
|
79
|
+
element: AstNode,
|
|
80
|
+
entityIdsByName: ReadonlyMap<string, string>,
|
|
81
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
82
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
83
|
+
userNamespace?: UserNamespaceContext
|
|
84
|
+
): string | null => {
|
|
85
|
+
if (element.type === 'Identifier') {
|
|
86
|
+
const name = identifierName(element);
|
|
87
|
+
return name
|
|
88
|
+
? deriveEntityIdentifierName(
|
|
89
|
+
name,
|
|
90
|
+
entityIdsByName,
|
|
91
|
+
knownEntityIds,
|
|
92
|
+
importAliases
|
|
93
|
+
)
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (userNamespace && userNamespace.bindings.size > 0) {
|
|
98
|
+
const namespaceTarget = resolveNamespaceMemberEntityName(
|
|
99
|
+
element,
|
|
100
|
+
userNamespace
|
|
101
|
+
);
|
|
102
|
+
if (namespaceTarget) {
|
|
103
|
+
return namespaceTarget;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return isEntityCall(element) ? extractFirstStringArg(element) : null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const extractDeclaredEntityNames = (
|
|
111
|
+
config: AstNode,
|
|
112
|
+
entityIdsByName: ReadonlyMap<string, string>,
|
|
113
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
114
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
115
|
+
userNamespace?: UserNamespaceContext
|
|
116
|
+
): readonly string[] => [
|
|
117
|
+
...new Set(
|
|
118
|
+
getEntityElements(config).flatMap((element) => {
|
|
119
|
+
const entityName = resolveDeclaredEntityName(
|
|
120
|
+
element,
|
|
121
|
+
entityIdsByName,
|
|
122
|
+
knownEntityIds,
|
|
123
|
+
importAliases,
|
|
124
|
+
userNamespace
|
|
125
|
+
);
|
|
126
|
+
return entityName ? [entityName] : [];
|
|
127
|
+
})
|
|
128
|
+
),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const buildMissingEntityDiagnostic = (
|
|
132
|
+
trailId: string,
|
|
133
|
+
entityName: string,
|
|
134
|
+
filePath: string,
|
|
135
|
+
line: number
|
|
136
|
+
): WardenDiagnostic => ({
|
|
137
|
+
filePath,
|
|
138
|
+
line,
|
|
139
|
+
message: `Trail "${trailId}" declares entity "${entityName}" which is not defined in the project. Define it with entity('${entityName}', ...) and include it in the topo, or fix the entities entry if this is a typo.`,
|
|
140
|
+
rule: 'entity-exists',
|
|
141
|
+
severity: 'error',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const buildDiagnosticsForDefinition = (
|
|
145
|
+
definition: TrailDefinition,
|
|
146
|
+
sourceCode: string,
|
|
147
|
+
filePath: string,
|
|
148
|
+
knownEntityIds: ReadonlySet<string>,
|
|
149
|
+
entityIdsByName: ReadonlyMap<string, string>,
|
|
150
|
+
importAliases: ReadonlyMap<string, string>,
|
|
151
|
+
userNamespace: UserNamespaceContext
|
|
152
|
+
): readonly WardenDiagnostic[] => {
|
|
153
|
+
if (definition.kind !== 'trail') {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const line = offsetToLine(sourceCode, definition.start);
|
|
158
|
+
return extractDeclaredEntityNames(
|
|
159
|
+
definition.config,
|
|
160
|
+
entityIdsByName,
|
|
161
|
+
knownEntityIds,
|
|
162
|
+
importAliases,
|
|
163
|
+
userNamespace
|
|
164
|
+
).flatMap((entityName) =>
|
|
165
|
+
knownEntityIds.has(entityName)
|
|
166
|
+
? []
|
|
167
|
+
: [
|
|
168
|
+
buildMissingEntityDiagnostic(
|
|
169
|
+
definition.id,
|
|
170
|
+
entityName,
|
|
171
|
+
filePath,
|
|
172
|
+
line
|
|
173
|
+
),
|
|
174
|
+
]
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const buildEntityDiagnostics = (
|
|
179
|
+
ast: AstNode,
|
|
180
|
+
sourceCode: string,
|
|
181
|
+
filePath: string,
|
|
182
|
+
knownEntityIds: ReadonlySet<string>
|
|
183
|
+
): readonly WardenDiagnostic[] => {
|
|
184
|
+
const entityIdsByName = collectNamedEntityIds(ast);
|
|
185
|
+
const importAliases = collectImportAliasMap(ast);
|
|
186
|
+
const userNamespace = buildUserNamespaceContext(ast);
|
|
187
|
+
|
|
188
|
+
return findTrailDefinitions(ast).flatMap((definition) =>
|
|
189
|
+
buildDiagnosticsForDefinition(
|
|
190
|
+
definition,
|
|
191
|
+
sourceCode,
|
|
192
|
+
filePath,
|
|
193
|
+
knownEntityIds,
|
|
194
|
+
entityIdsByName,
|
|
195
|
+
importAliases,
|
|
196
|
+
userNamespace
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const checkEntityDeclarations = (
|
|
202
|
+
ast: AstNode,
|
|
203
|
+
sourceCode: string,
|
|
204
|
+
filePath: string,
|
|
205
|
+
knownEntityIds: ReadonlySet<string>
|
|
206
|
+
): readonly WardenDiagnostic[] => {
|
|
207
|
+
if (isTestFile(filePath)) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return buildEntityDiagnostics(ast, sourceCode, filePath, knownEntityIds);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Checks that every entity declared in a trail `entities` array resolves to a
|
|
216
|
+
* known entity definition.
|
|
217
|
+
*/
|
|
218
|
+
export const entityExists: ProjectAwareWardenRule = {
|
|
219
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
220
|
+
const ast = parse(filePath, sourceCode);
|
|
221
|
+
if (!ast) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return checkEntityDeclarations(
|
|
226
|
+
ast,
|
|
227
|
+
sourceCode,
|
|
228
|
+
filePath,
|
|
229
|
+
collectEntityDefinitionIds(ast)
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
checkWithContext(
|
|
233
|
+
sourceCode: string,
|
|
234
|
+
filePath: string,
|
|
235
|
+
context: ProjectContext
|
|
236
|
+
): readonly WardenDiagnostic[] {
|
|
237
|
+
const ast = parse(filePath, sourceCode);
|
|
238
|
+
if (!ast) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const localEntityIds = collectEntityDefinitionIds(ast);
|
|
243
|
+
return checkEntityDeclarations(
|
|
244
|
+
ast,
|
|
245
|
+
sourceCode,
|
|
246
|
+
filePath,
|
|
247
|
+
mergeKnownEntityIds(localEntityIds, context.knownEntityIds)
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
description:
|
|
251
|
+
'Ensure every entity declared on a trail resolves to a known entity definition.',
|
|
252
|
+
name: 'entity-exists',
|
|
253
|
+
severity: 'error',
|
|
254
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge a file's locally-defined entity IDs with the project-wide set.
|
|
3
|
+
*
|
|
4
|
+
* Rules that run with a `ProjectContext` need to treat both local and
|
|
5
|
+
* project-wide entity definitions as "known" so that declarations and
|
|
6
|
+
* references resolve correctly. When no project context is available — e.g.
|
|
7
|
+
* single-file lint runs via `check` — the local set is returned as-is.
|
|
8
|
+
*/
|
|
9
|
+
export const mergeKnownEntityIds = (
|
|
10
|
+
localEntityIds: ReadonlySet<string>,
|
|
11
|
+
projectEntityIds?: ReadonlySet<string>
|
|
12
|
+
): ReadonlySet<string> =>
|
|
13
|
+
projectEntityIds
|
|
14
|
+
? new Set([...projectEntityIds, ...localEntityIds])
|
|
15
|
+
: localEntityIds;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that registered surface error mappers cover every error category.
|
|
3
|
+
*
|
|
4
|
+
* Scans `createSurfaceErrorMapper(...)` calls, then resolves simple object
|
|
5
|
+
* literals, identifier bindings, and object-property references in the same
|
|
6
|
+
* file so incomplete mapper registrations are caught before they ship.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { codesByCategory, errorClasses } from '@ontrails/core';
|
|
10
|
+
import type { ErrorCategory } from '@ontrails/core';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getNodeKey,
|
|
14
|
+
getNodeValueNode,
|
|
15
|
+
getStringValue,
|
|
16
|
+
identifierName,
|
|
17
|
+
isStringLiteral,
|
|
18
|
+
offsetToLine,
|
|
19
|
+
parse,
|
|
20
|
+
walk,
|
|
21
|
+
} from '@ontrails/source';
|
|
22
|
+
import type { AstNode } from '@ontrails/source';
|
|
23
|
+
import { isTestFile } from './scan.js';
|
|
24
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
25
|
+
|
|
26
|
+
const MEMBER_EXPRESSION_TYPES = new Set([
|
|
27
|
+
'MemberExpression',
|
|
28
|
+
'StaticMemberExpression',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const MAPPER_FACTORY_NAMES = new Set(['createSurfaceErrorMapper']);
|
|
32
|
+
|
|
33
|
+
const mappedErrorClassCategories = new Set(
|
|
34
|
+
errorClasses.flatMap((entry) =>
|
|
35
|
+
entry.category === 'dynamic' ? [] : [entry.category]
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const requiredErrorCategories = (
|
|
40
|
+
Object.keys(codesByCategory) as ErrorCategory[]
|
|
41
|
+
).filter((category) => mappedErrorClassCategories.has(category));
|
|
42
|
+
|
|
43
|
+
const getPropertyName = (node: AstNode | undefined): string | null => {
|
|
44
|
+
if (!node) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
identifierName(node) ??
|
|
50
|
+
(isStringLiteral(node) ? getStringValue(node) : null)
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const collectObjectBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
|
|
55
|
+
const bindings = new Map<string, AstNode>();
|
|
56
|
+
|
|
57
|
+
walk(ast, (node) => {
|
|
58
|
+
if (node.type !== 'VariableDeclarator') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { id, init } = node as { id?: AstNode; init?: AstNode };
|
|
63
|
+
const bindingName = identifierName(id);
|
|
64
|
+
|
|
65
|
+
if (bindingName && init?.type === 'ObjectExpression') {
|
|
66
|
+
bindings.set(bindingName, init);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return bindings;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getObjectProperties = (objectNode: AstNode): readonly AstNode[] =>
|
|
74
|
+
objectNode.type === 'ObjectExpression'
|
|
75
|
+
? ((objectNode['properties'] as readonly AstNode[] | undefined) ?? [])
|
|
76
|
+
: [];
|
|
77
|
+
|
|
78
|
+
const findObjectPropertyValue = (
|
|
79
|
+
objectNode: AstNode,
|
|
80
|
+
propertyName: string
|
|
81
|
+
): AstNode | null => {
|
|
82
|
+
for (const property of getObjectProperties(objectNode)) {
|
|
83
|
+
if (property.type !== 'Property') {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const key = getPropertyName(getNodeKey(property));
|
|
88
|
+
if (key === propertyName) {
|
|
89
|
+
return getNodeValueNode(property) ?? null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const resolveIdentifierObject = (
|
|
97
|
+
node: AstNode,
|
|
98
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
99
|
+
): AstNode | null =>
|
|
100
|
+
bindings.get((node as { name?: string }).name ?? '') ?? null;
|
|
101
|
+
|
|
102
|
+
const resolveMemberObject = (
|
|
103
|
+
node: AstNode,
|
|
104
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
105
|
+
depth: number,
|
|
106
|
+
resolve: (
|
|
107
|
+
node: AstNode | undefined,
|
|
108
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
109
|
+
depth?: number
|
|
110
|
+
) => AstNode | null
|
|
111
|
+
): AstNode | null => {
|
|
112
|
+
const { object, property } = node as { object?: AstNode; property?: AstNode };
|
|
113
|
+
const propertyName = getPropertyName(property);
|
|
114
|
+
if (!propertyName) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const objectNode = resolve(object, bindings, depth + 1);
|
|
119
|
+
return objectNode
|
|
120
|
+
? resolve(
|
|
121
|
+
findObjectPropertyValue(objectNode, propertyName) ?? undefined,
|
|
122
|
+
bindings,
|
|
123
|
+
depth + 1
|
|
124
|
+
)
|
|
125
|
+
: null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const resolveObjectExpression = function resolveObjectExpression(
|
|
129
|
+
node: AstNode | undefined,
|
|
130
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
131
|
+
depth = 0
|
|
132
|
+
): AstNode | null {
|
|
133
|
+
if (!node || depth > 4) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node.type === 'ObjectExpression') {
|
|
138
|
+
return node;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (node.type === 'Identifier') {
|
|
142
|
+
return resolveIdentifierObject(node, bindings);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return MEMBER_EXPRESSION_TYPES.has(node.type)
|
|
146
|
+
? resolveMemberObject(node, bindings, depth, resolveObjectExpression)
|
|
147
|
+
: null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const addMappedCategory = (
|
|
151
|
+
categories: Set<string>,
|
|
152
|
+
property: AstNode
|
|
153
|
+
): boolean => {
|
|
154
|
+
if (property.type === 'SpreadElement') {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (property.type !== 'Property') {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const key = getPropertyName(getNodeKey(property));
|
|
163
|
+
if (key) {
|
|
164
|
+
categories.add(key);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const collectMappedCategories = (
|
|
171
|
+
mapperObject: AstNode
|
|
172
|
+
): ReadonlySet<string> | null => {
|
|
173
|
+
if (mapperObject.type !== 'ObjectExpression') {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const categories = new Set<string>();
|
|
178
|
+
for (const property of getObjectProperties(mapperObject)) {
|
|
179
|
+
if (!addMappedCategory(categories, property)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return categories;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const createDiagnostic = (
|
|
188
|
+
filePath: string,
|
|
189
|
+
line: number,
|
|
190
|
+
missingCategories: readonly string[]
|
|
191
|
+
): WardenDiagnostic => ({
|
|
192
|
+
filePath,
|
|
193
|
+
line,
|
|
194
|
+
message: `Surface error mapper is missing mappings for: ${missingCategories.join(', ')}. Registered createSurfaceErrorMapper() calls must cover every ErrorCategory.`,
|
|
195
|
+
rule: 'error-mapping-completeness',
|
|
196
|
+
severity: 'error',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const getCallArgs = (node: AstNode): readonly AstNode[] =>
|
|
200
|
+
(node as { arguments?: readonly AstNode[] }).arguments ?? [];
|
|
201
|
+
|
|
202
|
+
const getCallCallee = (node: AstNode): AstNode | undefined =>
|
|
203
|
+
(node as { callee?: AstNode }).callee;
|
|
204
|
+
|
|
205
|
+
const isMapperFactoryCall = (node: AstNode): boolean =>
|
|
206
|
+
node.type === 'CallExpression' &&
|
|
207
|
+
MAPPER_FACTORY_NAMES.has(identifierName(getCallCallee(node)) ?? '');
|
|
208
|
+
|
|
209
|
+
const findMissingCategories = (
|
|
210
|
+
mappedCategories: ReadonlySet<string>
|
|
211
|
+
): readonly string[] =>
|
|
212
|
+
requiredErrorCategories.filter((category) => !mappedCategories.has(category));
|
|
213
|
+
|
|
214
|
+
const resolveMappedCategories = (
|
|
215
|
+
node: AstNode,
|
|
216
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
217
|
+
): ReadonlySet<string> | null => {
|
|
218
|
+
const [firstArg] = getCallArgs(node);
|
|
219
|
+
const mapperObject = resolveObjectExpression(firstArg, bindings);
|
|
220
|
+
return mapperObject ? collectMappedCategories(mapperObject) : null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const inspectMapperCall = (
|
|
224
|
+
node: AstNode,
|
|
225
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
226
|
+
filePath: string,
|
|
227
|
+
sourceCode: string
|
|
228
|
+
): WardenDiagnostic | null => {
|
|
229
|
+
if (!isMapperFactoryCall(node)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const mappedCategories = resolveMappedCategories(node, bindings);
|
|
234
|
+
if (!mappedCategories) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const missingCategories = findMissingCategories(mappedCategories);
|
|
239
|
+
if (missingCategories.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return createDiagnostic(
|
|
244
|
+
filePath,
|
|
245
|
+
offsetToLine(sourceCode, node.start),
|
|
246
|
+
missingCategories
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Flags registered surface error mapper calls that omit error categories.
|
|
252
|
+
*/
|
|
253
|
+
export const errorMappingCompleteness: WardenRule = {
|
|
254
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
255
|
+
if (
|
|
256
|
+
isTestFile(filePath) ||
|
|
257
|
+
![...MAPPER_FACTORY_NAMES].some((factoryName) =>
|
|
258
|
+
sourceCode.includes(factoryName)
|
|
259
|
+
)
|
|
260
|
+
) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const ast = parse(filePath, sourceCode);
|
|
265
|
+
if (!ast) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const bindings = collectObjectBindings(ast);
|
|
270
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
271
|
+
|
|
272
|
+
walk(ast, (node) => {
|
|
273
|
+
const diagnostic = inspectMapperCall(
|
|
274
|
+
node,
|
|
275
|
+
bindings,
|
|
276
|
+
filePath,
|
|
277
|
+
sourceCode
|
|
278
|
+
);
|
|
279
|
+
if (diagnostic) {
|
|
280
|
+
diagnostics.push(diagnostic);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return diagnostics;
|
|
285
|
+
},
|
|
286
|
+
description:
|
|
287
|
+
'Require registered surface error mappers to cover every ErrorCategory.',
|
|
288
|
+
name: 'error-mapping-completeness',
|
|
289
|
+
severity: 'error',
|
|
290
|
+
};
|