@ontrails/warden 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 +125 -0
- package/README.md +5 -5
- package/package.json +10 -12
- package/src/cli.ts +49 -44
- package/src/command.ts +2 -2
- package/src/drift.ts +4 -3
- package/src/index.ts +3 -2
- package/src/project-context.ts +3 -3
- package/src/resolve.ts +2 -2
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +12 -16
- package/src/rules/cli-command-route-coherence.ts +1 -1
- package/src/rules/composes-declarations.ts +15 -13
- package/src/rules/context-no-surface-types.ts +4 -3
- package/src/rules/dead-internal-trail.ts +3 -3
- package/src/rules/dead-public-trail.ts +3 -3
- package/src/rules/draft-file-marking.ts +4 -9
- package/src/rules/draft-visible-debt.ts +3 -8
- package/src/rules/duplicate-public-contract.ts +50 -5
- package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +2 -2
- package/src/rules/example-valid.ts +84 -84
- package/src/rules/fires-declarations.ts +27 -22
- package/src/rules/governed-symbol-residue.ts +339 -32
- package/src/rules/implementation-returns-result.ts +8 -8
- package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
- package/src/rules/incomplete-crud.ts +42 -40
- package/src/rules/index.ts +9 -6
- package/src/rules/intent-propagation.ts +3 -5
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/library-projection-coherence.ts +2 -2
- package/src/rules/metadata.ts +37 -15
- package/src/rules/missing-reconcile.ts +3 -4
- package/src/rules/missing-visibility.ts +2 -1
- package/src/rules/no-destructured-compose.ts +14 -12
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +2 -2
- package/src/rules/no-native-error-result.ts +2 -2
- package/src/rules/no-redundant-result-error-wrap.ts +10 -10
- package/src/rules/no-sync-result-assumption.ts +84 -78
- package/src/rules/no-throw-in-detour-recover.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +13 -7
- package/src/rules/no-top-level-surface.ts +2 -2
- package/src/rules/on-references-exist.ts +3 -3
- package/src/rules/orphaned-signal.ts +4 -5
- package/src/rules/owner-projection-parity.ts +2 -2
- package/src/rules/public-export-example-coverage.ts +10 -4
- package/src/rules/public-internal-deep-imports.ts +2 -3
- package/src/rules/read-intent-fires.ts +4 -3
- package/src/rules/reference-exists.ts +20 -21
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/resolved-import-boundary.ts +1 -1
- package/src/rules/resource-declarations.ts +21 -17
- package/src/rules/resource-exists.ts +4 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/retired-vocabulary.ts +242 -5
- 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/static-resource-accessor-preference.ts +15 -11
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/surface-trailhead-coherence.ts +2 -2
- package/src/rules/trail-fork-coaching.ts +27 -18
- package/src/rules/trail-versioning-source.ts +16 -12
- package/src/rules/trail-versioning-topo.ts +2 -2
- package/src/rules/trailhead-override-divergence.ts +2 -2
- package/src/rules/types.ts +7 -7
- package/src/rules/unmaterialized-activation-source.ts +1 -0
- package/src/rules/unreachable-detour-shadowing.ts +2 -2
- package/src/rules/valid-describe-refs.ts +2 -2
- package/src/rules/warden-export-symmetry.ts +3 -3
- package/src/rules/warden-rules-use-ast.ts +12 -17
- package/src/trails/activation-orphan.trail.ts +5 -5
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +6 -6
- package/src/trails/cli-command-route-coherence.trail.ts +2 -2
- package/src/trails/composes-declarations.trail.ts +1 -1
- package/src/trails/context-no-surface-types.trail.ts +1 -1
- package/src/trails/dead-internal-trail.trail.ts +2 -2
- package/src/trails/dead-public-trail.trail.ts +1 -1
- package/src/trails/deprecation-without-guidance.trail.ts +1 -1
- package/src/trails/duplicate-public-contract.trail.ts +3 -3
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/example-valid.trail.ts +3 -3
- package/src/trails/fires-declarations.trail.ts +1 -1
- package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
- package/src/trails/index.ts +3 -2
- package/src/trails/intent-propagation.trail.ts +2 -2
- package/src/trails/library-projection-coherence.trail.ts +2 -2
- package/src/trails/marker-schema-unsupported.trail.ts +1 -1
- package/src/trails/missing-visibility.trail.ts +1 -1
- package/src/trails/no-destructured-compose.trail.ts +5 -5
- package/src/trails/no-direct-implementation-call.trail.ts +1 -1
- package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/on-references-exist.trail.ts +1 -1
- package/src/trails/pending-force.trail.ts +1 -1
- package/src/trails/permit-governance.trail.ts +2 -2
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/public-output-schema.trail.ts +2 -2
- package/src/trails/public-union-output-discriminants.trail.ts +1 -1
- package/src/trails/read-intent-fires.trail.ts +1 -1
- package/src/trails/reference-exists.trail.ts +5 -5
- package/src/trails/resource-declarations.trail.ts +1 -1
- package/src/trails/resource-exists.trail.ts +1 -1
- package/src/trails/run.ts +4 -6
- package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
- package/src/trails/schema.ts +9 -9
- package/src/trails/signal-graph-coaching.trail.ts +2 -2
- package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
- package/src/trails/surface-overlay-coherence.trail.ts +1 -1
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
- package/src/trails/valid-detour-contract.trail.ts +1 -1
- package/src/trails/version-gap.trail.ts +1 -1
- package/src/trails/version-pinned-compose.trail.ts +1 -1
- package/src/trails/version-without-examples.trail.ts +1 -1
- package/src/trails/warden-rules-use-ast.trail.ts +2 -2
- package/src/trails/webhook-route-collision.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/src/ast.ts +0 -137
- package/src/rules/ast.ts +0 -4143
- package/src/rules/contour-ids.ts +0 -15
- package/src/trails/contour-exists.trail.ts +0 -21
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { deriveTopoGraph } from '@ontrails/
|
|
2
|
-
import type { TopoGraph, TopoGraphEntry } from '@ontrails/
|
|
1
|
+
import { deriveTopoGraph } from '@ontrails/topography';
|
|
2
|
+
import type { TopoGraph, TopoGraphEntry } from '@ontrails/topography';
|
|
3
3
|
|
|
4
4
|
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
5
5
|
|
|
6
6
|
const RULE_NAME = 'duplicate-public-contract';
|
|
7
7
|
const TOPO_FILE = '<topo>';
|
|
8
|
+
const INVERSE_OPERATION_PAIRS = new Map([
|
|
9
|
+
['archive', 'restore'],
|
|
10
|
+
['disable', 'enable'],
|
|
11
|
+
['pause', 'resume'],
|
|
12
|
+
['star', 'unstar'],
|
|
13
|
+
]);
|
|
8
14
|
|
|
9
15
|
const resolveGraph = (
|
|
10
16
|
topo: Parameters<TopoAwareWardenRule['checkTopo']>[0],
|
|
@@ -30,9 +36,9 @@ const contractFingerprint = (entry: TopoGraphEntry): string =>
|
|
|
30
36
|
JSON.stringify(
|
|
31
37
|
canonicalize({
|
|
32
38
|
composes: entry.composes,
|
|
33
|
-
contours: entry.contours,
|
|
34
39
|
detours: entry.detours,
|
|
35
40
|
dryRunCapable: entry.dryRunCapable,
|
|
41
|
+
entities: entry.entities,
|
|
36
42
|
fires: entry.fires,
|
|
37
43
|
idempotent: entry.idempotent,
|
|
38
44
|
input: entry.input,
|
|
@@ -60,10 +66,46 @@ const renderTrailIds = (trailIds: readonly string[]): string =>
|
|
|
60
66
|
.map((trailId) => `"${trailId}"`)
|
|
61
67
|
.join(', ');
|
|
62
68
|
|
|
69
|
+
const splitOperation = (
|
|
70
|
+
trailId: string
|
|
71
|
+
): { readonly scope: string; readonly operation: string } | undefined => {
|
|
72
|
+
const separatorIndex = trailId.lastIndexOf('.');
|
|
73
|
+
|
|
74
|
+
if (separatorIndex <= 0 || separatorIndex === trailId.length - 1) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
operation: trailId.slice(separatorIndex + 1),
|
|
80
|
+
scope: trailId.slice(0, separatorIndex),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const areInverseOperations = (left: string, right: string): boolean =>
|
|
85
|
+
INVERSE_OPERATION_PAIRS.get(left) === right ||
|
|
86
|
+
INVERSE_OPERATION_PAIRS.get(right) === left;
|
|
87
|
+
|
|
88
|
+
const isAcceptedInverseOperationPair = (
|
|
89
|
+
trailIds: readonly string[]
|
|
90
|
+
): boolean => {
|
|
91
|
+
if (trailIds.length !== 2) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const [left, right] = trailIds.map(splitOperation);
|
|
96
|
+
|
|
97
|
+
return Boolean(
|
|
98
|
+
left &&
|
|
99
|
+
right &&
|
|
100
|
+
left.scope === right.scope &&
|
|
101
|
+
areInverseOperations(left.operation, right.operation)
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
63
105
|
const buildDiagnostic = (trailIds: readonly string[]): WardenDiagnostic => ({
|
|
64
106
|
filePath: TOPO_FILE,
|
|
65
107
|
line: 1,
|
|
66
|
-
message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources,
|
|
108
|
+
message: `Likely duplicate public trail contracts ${renderTrailIds(trailIds)} share the same input, output, intent, permits, resources, entities, composes, signals, and detours. Keep one contract with aliases/input mappings, compose a distinct wrapper, or document why these public contracts are separate.`,
|
|
67
109
|
rule: RULE_NAME,
|
|
68
110
|
severity: 'warn',
|
|
69
111
|
});
|
|
@@ -82,7 +124,10 @@ export const duplicatePublicContract: TopoAwareWardenRule = {
|
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
return [...groups.values()]
|
|
85
|
-
.filter(
|
|
127
|
+
.filter(
|
|
128
|
+
(trailIds) =>
|
|
129
|
+
trailIds.length > 1 && !isAcceptedInverseOperationPair(trailIds)
|
|
130
|
+
)
|
|
86
131
|
.map(buildDiagnostic);
|
|
87
132
|
},
|
|
88
133
|
description:
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildUserNamespaceContext,
|
|
3
|
-
|
|
3
|
+
collectEntityDefinitionIds,
|
|
4
4
|
collectImportAliasMap,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
collectNamedEntityIds,
|
|
6
|
+
deriveEntityIdentifierName,
|
|
7
|
+
isUserNamespaceReceiverAllowed,
|
|
8
|
+
} from './source/entities.js';
|
|
9
|
+
import {
|
|
7
10
|
extractFirstStringArg,
|
|
8
11
|
findConfigProperty,
|
|
9
12
|
findTrailDefinitions,
|
|
@@ -12,12 +15,12 @@ import {
|
|
|
12
15
|
getNodeProperty,
|
|
13
16
|
identifierName,
|
|
14
17
|
isMemberAccessNonComputed,
|
|
15
|
-
isUserNamespaceReceiverAllowed,
|
|
16
18
|
offsetToLine,
|
|
17
19
|
parse,
|
|
18
|
-
} from '
|
|
19
|
-
import type { AstNode, TrailDefinition
|
|
20
|
-
import {
|
|
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';
|
|
21
24
|
import { isTestFile } from './scan.js';
|
|
22
25
|
import type {
|
|
23
26
|
ProjectAwareWardenRule,
|
|
@@ -25,17 +28,17 @@ import type {
|
|
|
25
28
|
WardenDiagnostic,
|
|
26
29
|
} from './types.js';
|
|
27
30
|
|
|
28
|
-
const
|
|
31
|
+
const isEntityCall = (node: AstNode): boolean =>
|
|
29
32
|
node.type === 'CallExpression' &&
|
|
30
|
-
identifierName(getNodeCallee(node)) === '
|
|
33
|
+
identifierName(getNodeCallee(node)) === 'entity';
|
|
31
34
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
+
const getEntityElements = (config: AstNode): readonly AstNode[] => {
|
|
36
|
+
const entitiesProp = findConfigProperty(config, 'entities');
|
|
37
|
+
if (!entitiesProp) {
|
|
35
38
|
return [];
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
const arrayNode =
|
|
41
|
+
const arrayNode = entitiesProp.value;
|
|
39
42
|
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
40
43
|
return [];
|
|
41
44
|
}
|
|
@@ -47,13 +50,13 @@ const getContourElements = (config: AstNode): readonly AstNode[] => {
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
|
-
* Resolve `
|
|
53
|
+
* Resolve `entities.user` to its entity name. When `userNamespace` carries a
|
|
51
54
|
* scope-aware `safeMemberStarts` set, the member access must appear in it —
|
|
52
|
-
* rejecting cases where `
|
|
53
|
-
* function parameter or `const
|
|
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
|
|
54
57
|
* to the bare name check for backward compatibility.
|
|
55
58
|
*/
|
|
56
|
-
const
|
|
59
|
+
const resolveNamespaceMemberEntityName = (
|
|
57
60
|
element: AstNode,
|
|
58
61
|
userNamespace: UserNamespaceContext
|
|
59
62
|
): string | null => {
|
|
@@ -72,27 +75,27 @@ const resolveNamespaceMemberContourName = (
|
|
|
72
75
|
return property ? identifierName(property) : null;
|
|
73
76
|
};
|
|
74
77
|
|
|
75
|
-
const
|
|
78
|
+
const resolveDeclaredEntityName = (
|
|
76
79
|
element: AstNode,
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
entityIdsByName: ReadonlyMap<string, string>,
|
|
81
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
79
82
|
importAliases?: ReadonlyMap<string, string>,
|
|
80
83
|
userNamespace?: UserNamespaceContext
|
|
81
84
|
): string | null => {
|
|
82
85
|
if (element.type === 'Identifier') {
|
|
83
86
|
const name = identifierName(element);
|
|
84
87
|
return name
|
|
85
|
-
?
|
|
88
|
+
? deriveEntityIdentifierName(
|
|
86
89
|
name,
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
entityIdsByName,
|
|
91
|
+
knownEntityIds,
|
|
89
92
|
importAliases
|
|
90
93
|
)
|
|
91
94
|
: null;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
if (userNamespace && userNamespace.bindings.size > 0) {
|
|
95
|
-
const namespaceTarget =
|
|
98
|
+
const namespaceTarget = resolveNamespaceMemberEntityName(
|
|
96
99
|
element,
|
|
97
100
|
userNamespace
|
|
98
101
|
);
|
|
@@ -101,40 +104,40 @@ const resolveDeclaredContourName = (
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
return
|
|
107
|
+
return isEntityCall(element) ? extractFirstStringArg(element) : null;
|
|
105
108
|
};
|
|
106
109
|
|
|
107
|
-
const
|
|
110
|
+
const extractDeclaredEntityNames = (
|
|
108
111
|
config: AstNode,
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
entityIdsByName: ReadonlyMap<string, string>,
|
|
113
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
111
114
|
importAliases?: ReadonlyMap<string, string>,
|
|
112
115
|
userNamespace?: UserNamespaceContext
|
|
113
116
|
): readonly string[] => [
|
|
114
117
|
...new Set(
|
|
115
|
-
|
|
116
|
-
const
|
|
118
|
+
getEntityElements(config).flatMap((element) => {
|
|
119
|
+
const entityName = resolveDeclaredEntityName(
|
|
117
120
|
element,
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
entityIdsByName,
|
|
122
|
+
knownEntityIds,
|
|
120
123
|
importAliases,
|
|
121
124
|
userNamespace
|
|
122
125
|
);
|
|
123
|
-
return
|
|
126
|
+
return entityName ? [entityName] : [];
|
|
124
127
|
})
|
|
125
128
|
),
|
|
126
129
|
];
|
|
127
130
|
|
|
128
|
-
const
|
|
131
|
+
const buildMissingEntityDiagnostic = (
|
|
129
132
|
trailId: string,
|
|
130
|
-
|
|
133
|
+
entityName: string,
|
|
131
134
|
filePath: string,
|
|
132
135
|
line: number
|
|
133
136
|
): WardenDiagnostic => ({
|
|
134
137
|
filePath,
|
|
135
138
|
line,
|
|
136
|
-
message: `Trail "${trailId}" declares
|
|
137
|
-
rule: '
|
|
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',
|
|
138
141
|
severity: 'error',
|
|
139
142
|
});
|
|
140
143
|
|
|
@@ -142,8 +145,8 @@ const buildDiagnosticsForDefinition = (
|
|
|
142
145
|
definition: TrailDefinition,
|
|
143
146
|
sourceCode: string,
|
|
144
147
|
filePath: string,
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
knownEntityIds: ReadonlySet<string>,
|
|
149
|
+
entityIdsByName: ReadonlyMap<string, string>,
|
|
147
150
|
importAliases: ReadonlyMap<string, string>,
|
|
148
151
|
userNamespace: UserNamespaceContext
|
|
149
152
|
): readonly WardenDiagnostic[] => {
|
|
@@ -152,19 +155,19 @@ const buildDiagnosticsForDefinition = (
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
const line = offsetToLine(sourceCode, definition.start);
|
|
155
|
-
return
|
|
158
|
+
return extractDeclaredEntityNames(
|
|
156
159
|
definition.config,
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
entityIdsByName,
|
|
161
|
+
knownEntityIds,
|
|
159
162
|
importAliases,
|
|
160
163
|
userNamespace
|
|
161
|
-
).flatMap((
|
|
162
|
-
|
|
164
|
+
).flatMap((entityName) =>
|
|
165
|
+
knownEntityIds.has(entityName)
|
|
163
166
|
? []
|
|
164
167
|
: [
|
|
165
|
-
|
|
168
|
+
buildMissingEntityDiagnostic(
|
|
166
169
|
definition.id,
|
|
167
|
-
|
|
170
|
+
entityName,
|
|
168
171
|
filePath,
|
|
169
172
|
line
|
|
170
173
|
),
|
|
@@ -172,13 +175,13 @@ const buildDiagnosticsForDefinition = (
|
|
|
172
175
|
);
|
|
173
176
|
};
|
|
174
177
|
|
|
175
|
-
const
|
|
178
|
+
const buildEntityDiagnostics = (
|
|
176
179
|
ast: AstNode,
|
|
177
180
|
sourceCode: string,
|
|
178
181
|
filePath: string,
|
|
179
|
-
|
|
182
|
+
knownEntityIds: ReadonlySet<string>
|
|
180
183
|
): readonly WardenDiagnostic[] => {
|
|
181
|
-
const
|
|
184
|
+
const entityIdsByName = collectNamedEntityIds(ast);
|
|
182
185
|
const importAliases = collectImportAliasMap(ast);
|
|
183
186
|
const userNamespace = buildUserNamespaceContext(ast);
|
|
184
187
|
|
|
@@ -187,43 +190,43 @@ const buildContourDiagnostics = (
|
|
|
187
190
|
definition,
|
|
188
191
|
sourceCode,
|
|
189
192
|
filePath,
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
knownEntityIds,
|
|
194
|
+
entityIdsByName,
|
|
192
195
|
importAliases,
|
|
193
196
|
userNamespace
|
|
194
197
|
)
|
|
195
198
|
);
|
|
196
199
|
};
|
|
197
200
|
|
|
198
|
-
const
|
|
201
|
+
const checkEntityDeclarations = (
|
|
199
202
|
ast: AstNode,
|
|
200
203
|
sourceCode: string,
|
|
201
204
|
filePath: string,
|
|
202
|
-
|
|
205
|
+
knownEntityIds: ReadonlySet<string>
|
|
203
206
|
): readonly WardenDiagnostic[] => {
|
|
204
207
|
if (isTestFile(filePath)) {
|
|
205
208
|
return [];
|
|
206
209
|
}
|
|
207
210
|
|
|
208
|
-
return
|
|
211
|
+
return buildEntityDiagnostics(ast, sourceCode, filePath, knownEntityIds);
|
|
209
212
|
};
|
|
210
213
|
|
|
211
214
|
/**
|
|
212
|
-
* Checks that every
|
|
213
|
-
* known
|
|
215
|
+
* Checks that every entity declared in a trail `entities` array resolves to a
|
|
216
|
+
* known entity definition.
|
|
214
217
|
*/
|
|
215
|
-
export const
|
|
218
|
+
export const entityExists: ProjectAwareWardenRule = {
|
|
216
219
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
217
220
|
const ast = parse(filePath, sourceCode);
|
|
218
221
|
if (!ast) {
|
|
219
222
|
return [];
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
return
|
|
225
|
+
return checkEntityDeclarations(
|
|
223
226
|
ast,
|
|
224
227
|
sourceCode,
|
|
225
228
|
filePath,
|
|
226
|
-
|
|
229
|
+
collectEntityDefinitionIds(ast)
|
|
227
230
|
);
|
|
228
231
|
},
|
|
229
232
|
checkWithContext(
|
|
@@ -236,16 +239,16 @@ export const contourExists: ProjectAwareWardenRule = {
|
|
|
236
239
|
return [];
|
|
237
240
|
}
|
|
238
241
|
|
|
239
|
-
const
|
|
240
|
-
return
|
|
242
|
+
const localEntityIds = collectEntityDefinitionIds(ast);
|
|
243
|
+
return checkEntityDeclarations(
|
|
241
244
|
ast,
|
|
242
245
|
sourceCode,
|
|
243
246
|
filePath,
|
|
244
|
-
|
|
247
|
+
mergeKnownEntityIds(localEntityIds, context.knownEntityIds)
|
|
245
248
|
);
|
|
246
249
|
},
|
|
247
250
|
description:
|
|
248
|
-
'Ensure every
|
|
249
|
-
name: '
|
|
251
|
+
'Ensure every entity declared on a trail resolves to a known entity definition.',
|
|
252
|
+
name: 'entity-exists',
|
|
250
253
|
severity: 'error',
|
|
251
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;
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
offsetToLine,
|
|
19
19
|
parse,
|
|
20
20
|
walk,
|
|
21
|
-
} from '
|
|
22
|
-
import type { AstNode } from '
|
|
21
|
+
} from '@ontrails/source';
|
|
22
|
+
import type { AstNode } from '@ontrails/source';
|
|
23
23
|
import { isTestFile } from './scan.js';
|
|
24
24
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
25
25
|
|