@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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 +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -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 +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -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/error-mapping-completeness.trail.ts +29 -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-blaze.trail.ts +31 -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 +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -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-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 +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -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-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -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 +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -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 -221
- 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 -163
- 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 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- 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 -46
- 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 -75
- 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 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- 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
|
@@ -40,6 +40,67 @@ interface ImportSpecifier {
|
|
|
40
40
|
readonly imported?: { readonly name?: string };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const isBareTrailCallee = (callee: AstNode): boolean => {
|
|
44
|
+
if (callee.type !== 'Identifier') {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return (callee as unknown as { name?: string }).name === 'trail';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isNamespacedTrailCallee = (callee: AstNode): boolean => {
|
|
51
|
+
if (
|
|
52
|
+
callee.type !== 'MemberExpression' &&
|
|
53
|
+
callee.type !== 'StaticMemberExpression'
|
|
54
|
+
) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Skip computed access like `ns[trail]()` — the bracketed expression may
|
|
58
|
+
// resolve to any runtime value, not the `trail` primitive, even when it
|
|
59
|
+
// happens to be an identifier literally named `trail`.
|
|
60
|
+
if ((callee as unknown as { computed?: boolean }).computed === true) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const prop = (callee as unknown as { property?: AstNode }).property;
|
|
64
|
+
if (prop?.type !== 'Identifier') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return (prop as unknown as { name?: string }).name === 'trail';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* True when `ast` contains a `trail(...)` call expression — i.e. this file
|
|
72
|
+
* looks like a trail definition. AST-based replacement for the legacy
|
|
73
|
+
* `/\btrail\s*\(/.test(sourceCode)` gate, which fired on string literals,
|
|
74
|
+
* comments, and docstrings.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Both bare-identifier `trail(...)` and namespaced `ns.trail(...)` callees
|
|
78
|
+
* are recognized, so files using either `import { trail }` or
|
|
79
|
+
* `import * as ns from '@ontrails/core'` are detected as trail definitions.
|
|
80
|
+
*
|
|
81
|
+
* The inner `if (found)` guard skips further work in each callback invocation,
|
|
82
|
+
* but the shared `walk` helper in `./ast.ts` exposes no abort mechanism, so
|
|
83
|
+
* the full tree is still traversed once a match is seen. Acceptable: `walk`
|
|
84
|
+
* is cheap and this rule only runs on files that already matched a path
|
|
85
|
+
* filter upstream.
|
|
86
|
+
*/
|
|
87
|
+
const hasTrailCall = (ast: AstNode): boolean => {
|
|
88
|
+
let found = false;
|
|
89
|
+
walk(ast, (node) => {
|
|
90
|
+
if (found || node.type !== 'CallExpression') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { callee } = node as unknown as { callee?: AstNode };
|
|
94
|
+
if (!callee) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isBareTrailCallee(callee) || isNamespacedTrailCallee(callee)) {
|
|
98
|
+
found = true;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return found;
|
|
102
|
+
};
|
|
103
|
+
|
|
43
104
|
const makeDiag = (
|
|
44
105
|
filePath: string,
|
|
45
106
|
sourceCode: string,
|
|
@@ -92,7 +153,7 @@ const checkSpecifiersForSurfaceTypes = (
|
|
|
92
153
|
filePath,
|
|
93
154
|
sourceCode,
|
|
94
155
|
node,
|
|
95
|
-
`Do not import surface type "${typeName}" in trail
|
|
156
|
+
`Do not import surface type "${typeName}" in trail files.`
|
|
96
157
|
);
|
|
97
158
|
};
|
|
98
159
|
|
|
@@ -111,7 +172,7 @@ const classifyImport = (
|
|
|
111
172
|
filePath,
|
|
112
173
|
sourceCode,
|
|
113
174
|
node,
|
|
114
|
-
`Do not import from surface module "${moduleName}" in trail
|
|
175
|
+
`Do not import from surface module "${moduleName}" in trail files.`
|
|
115
176
|
);
|
|
116
177
|
}
|
|
117
178
|
|
|
@@ -119,18 +180,17 @@ const classifyImport = (
|
|
|
119
180
|
};
|
|
120
181
|
|
|
121
182
|
/**
|
|
122
|
-
* Detects imports of surface-specific types in trail
|
|
183
|
+
* Detects imports of surface-specific types in trail files.
|
|
123
184
|
*/
|
|
124
185
|
export const contextNoSurfaceTypes: WardenRule = {
|
|
125
186
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
126
|
-
if (!/\b(?:trail|hike)\s*\(/.test(sourceCode)) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
187
|
const ast = parse(filePath, sourceCode);
|
|
131
188
|
if (!ast) {
|
|
132
189
|
return [];
|
|
133
190
|
}
|
|
191
|
+
if (!hasTrailCall(ast)) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
134
194
|
|
|
135
195
|
const diagnostics: WardenDiagnostic[] = [];
|
|
136
196
|
walk(ast, (node) => {
|
|
@@ -143,7 +203,7 @@ export const contextNoSurfaceTypes: WardenRule = {
|
|
|
143
203
|
return diagnostics;
|
|
144
204
|
},
|
|
145
205
|
description:
|
|
146
|
-
'Disallow surface-specific type imports (Request, Response, McpSession, etc.) in trail
|
|
206
|
+
'Disallow surface-specific type imports (Request, Response, McpSession, etc.) in trail files.',
|
|
147
207
|
name: 'context-no-surface-types',
|
|
148
208
|
|
|
149
209
|
severity: 'error',
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildUserNamespaceContext,
|
|
3
|
+
collectContourDefinitionIds,
|
|
4
|
+
collectImportAliasMap,
|
|
5
|
+
collectNamedContourIds,
|
|
6
|
+
extractFirstStringArg,
|
|
7
|
+
findConfigProperty,
|
|
8
|
+
findTrailDefinitions,
|
|
9
|
+
identifierName,
|
|
10
|
+
isMemberAccessNonComputed,
|
|
11
|
+
isUserNamespaceReceiverAllowed,
|
|
12
|
+
offsetToLine,
|
|
13
|
+
parse,
|
|
14
|
+
deriveContourIdentifierName,
|
|
15
|
+
} from './ast.js';
|
|
16
|
+
import type { AstNode, TrailDefinition, UserNamespaceContext } from './ast.js';
|
|
17
|
+
import { mergeKnownContourIds } from './contour-ids.js';
|
|
18
|
+
import { isTestFile } from './scan.js';
|
|
19
|
+
import type {
|
|
20
|
+
ProjectAwareWardenRule,
|
|
21
|
+
ProjectContext,
|
|
22
|
+
WardenDiagnostic,
|
|
23
|
+
} from './types.js';
|
|
24
|
+
|
|
25
|
+
const isContourCall = (node: AstNode): boolean =>
|
|
26
|
+
node.type === 'CallExpression' &&
|
|
27
|
+
identifierName((node as unknown as { callee?: AstNode }).callee) ===
|
|
28
|
+
'contour';
|
|
29
|
+
|
|
30
|
+
const getContourElements = (config: AstNode): readonly AstNode[] => {
|
|
31
|
+
const contoursProp = findConfigProperty(config, 'contours');
|
|
32
|
+
if (!contoursProp) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const arrayNode = contoursProp.value;
|
|
37
|
+
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const elements = (arrayNode as AstNode)['elements'] as
|
|
42
|
+
| readonly AstNode[]
|
|
43
|
+
| undefined;
|
|
44
|
+
return elements ?? [];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `contours.user` to its contour name. When `userNamespace` carries a
|
|
49
|
+
* scope-aware `safeMemberStarts` set, the member access must appear in it —
|
|
50
|
+
* rejecting cases where `contours` is shadowed by a local binding such as a
|
|
51
|
+
* function parameter or `const contours = ...`. Without the set, falls back
|
|
52
|
+
* to the bare name check for backward compatibility.
|
|
53
|
+
*/
|
|
54
|
+
const resolveNamespaceMemberContourName = (
|
|
55
|
+
element: AstNode,
|
|
56
|
+
userNamespace: UserNamespaceContext
|
|
57
|
+
): string | null => {
|
|
58
|
+
if (!isMemberAccessNonComputed(element)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const { object, property } = element as unknown as {
|
|
62
|
+
readonly object?: AstNode;
|
|
63
|
+
readonly property?: AstNode;
|
|
64
|
+
};
|
|
65
|
+
const receiver = object ? identifierName(object) : null;
|
|
66
|
+
if (
|
|
67
|
+
!receiver ||
|
|
68
|
+
!isUserNamespaceReceiverAllowed(receiver, element.start, userNamespace)
|
|
69
|
+
) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return property ? identifierName(property) : null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const resolveDeclaredContourName = (
|
|
76
|
+
element: AstNode,
|
|
77
|
+
contourIdsByName: ReadonlyMap<string, string>,
|
|
78
|
+
knownContourIds?: ReadonlySet<string>,
|
|
79
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
80
|
+
userNamespace?: UserNamespaceContext
|
|
81
|
+
): string | null => {
|
|
82
|
+
if (element.type === 'Identifier') {
|
|
83
|
+
const name = identifierName(element);
|
|
84
|
+
return name
|
|
85
|
+
? deriveContourIdentifierName(
|
|
86
|
+
name,
|
|
87
|
+
contourIdsByName,
|
|
88
|
+
knownContourIds,
|
|
89
|
+
importAliases
|
|
90
|
+
)
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (userNamespace && userNamespace.bindings.size > 0) {
|
|
95
|
+
const namespaceTarget = resolveNamespaceMemberContourName(
|
|
96
|
+
element,
|
|
97
|
+
userNamespace
|
|
98
|
+
);
|
|
99
|
+
if (namespaceTarget) {
|
|
100
|
+
return namespaceTarget;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return isContourCall(element) ? extractFirstStringArg(element) : null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const extractDeclaredContourNames = (
|
|
108
|
+
config: AstNode,
|
|
109
|
+
contourIdsByName: ReadonlyMap<string, string>,
|
|
110
|
+
knownContourIds?: ReadonlySet<string>,
|
|
111
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
112
|
+
userNamespace?: UserNamespaceContext
|
|
113
|
+
): readonly string[] => [
|
|
114
|
+
...new Set(
|
|
115
|
+
getContourElements(config).flatMap((element) => {
|
|
116
|
+
const contourName = resolveDeclaredContourName(
|
|
117
|
+
element,
|
|
118
|
+
contourIdsByName,
|
|
119
|
+
knownContourIds,
|
|
120
|
+
importAliases,
|
|
121
|
+
userNamespace
|
|
122
|
+
);
|
|
123
|
+
return contourName ? [contourName] : [];
|
|
124
|
+
})
|
|
125
|
+
),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const buildMissingContourDiagnostic = (
|
|
129
|
+
trailId: string,
|
|
130
|
+
contourName: string,
|
|
131
|
+
filePath: string,
|
|
132
|
+
line: number
|
|
133
|
+
): WardenDiagnostic => ({
|
|
134
|
+
filePath,
|
|
135
|
+
line,
|
|
136
|
+
message: `Trail "${trailId}" declares contour "${contourName}" which is not defined in the project. Define it with contour('${contourName}', ...) and include it in the topo, or fix the contours entry if this is a typo.`,
|
|
137
|
+
rule: 'contour-exists',
|
|
138
|
+
severity: 'error',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const buildDiagnosticsForDefinition = (
|
|
142
|
+
definition: TrailDefinition,
|
|
143
|
+
sourceCode: string,
|
|
144
|
+
filePath: string,
|
|
145
|
+
knownContourIds: ReadonlySet<string>,
|
|
146
|
+
contourIdsByName: ReadonlyMap<string, string>,
|
|
147
|
+
importAliases: ReadonlyMap<string, string>,
|
|
148
|
+
userNamespace: UserNamespaceContext
|
|
149
|
+
): readonly WardenDiagnostic[] => {
|
|
150
|
+
if (definition.kind !== 'trail') {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const line = offsetToLine(sourceCode, definition.start);
|
|
155
|
+
return extractDeclaredContourNames(
|
|
156
|
+
definition.config,
|
|
157
|
+
contourIdsByName,
|
|
158
|
+
knownContourIds,
|
|
159
|
+
importAliases,
|
|
160
|
+
userNamespace
|
|
161
|
+
).flatMap((contourName) =>
|
|
162
|
+
knownContourIds.has(contourName)
|
|
163
|
+
? []
|
|
164
|
+
: [
|
|
165
|
+
buildMissingContourDiagnostic(
|
|
166
|
+
definition.id,
|
|
167
|
+
contourName,
|
|
168
|
+
filePath,
|
|
169
|
+
line
|
|
170
|
+
),
|
|
171
|
+
]
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const buildContourDiagnostics = (
|
|
176
|
+
ast: AstNode,
|
|
177
|
+
sourceCode: string,
|
|
178
|
+
filePath: string,
|
|
179
|
+
knownContourIds: ReadonlySet<string>
|
|
180
|
+
): readonly WardenDiagnostic[] => {
|
|
181
|
+
const contourIdsByName = collectNamedContourIds(ast);
|
|
182
|
+
const importAliases = collectImportAliasMap(ast);
|
|
183
|
+
const userNamespace = buildUserNamespaceContext(ast);
|
|
184
|
+
|
|
185
|
+
return findTrailDefinitions(ast).flatMap((definition) =>
|
|
186
|
+
buildDiagnosticsForDefinition(
|
|
187
|
+
definition,
|
|
188
|
+
sourceCode,
|
|
189
|
+
filePath,
|
|
190
|
+
knownContourIds,
|
|
191
|
+
contourIdsByName,
|
|
192
|
+
importAliases,
|
|
193
|
+
userNamespace
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const checkContourDeclarations = (
|
|
199
|
+
ast: AstNode,
|
|
200
|
+
sourceCode: string,
|
|
201
|
+
filePath: string,
|
|
202
|
+
knownContourIds: ReadonlySet<string>
|
|
203
|
+
): readonly WardenDiagnostic[] => {
|
|
204
|
+
if (isTestFile(filePath)) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return buildContourDiagnostics(ast, sourceCode, filePath, knownContourIds);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Checks that every contour declared in a trail `contours` array resolves to a
|
|
213
|
+
* known contour definition.
|
|
214
|
+
*/
|
|
215
|
+
export const contourExists: ProjectAwareWardenRule = {
|
|
216
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
217
|
+
const ast = parse(filePath, sourceCode);
|
|
218
|
+
if (!ast) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return checkContourDeclarations(
|
|
223
|
+
ast,
|
|
224
|
+
sourceCode,
|
|
225
|
+
filePath,
|
|
226
|
+
collectContourDefinitionIds(ast)
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
checkWithContext(
|
|
230
|
+
sourceCode: string,
|
|
231
|
+
filePath: string,
|
|
232
|
+
context: ProjectContext
|
|
233
|
+
): readonly WardenDiagnostic[] {
|
|
234
|
+
const ast = parse(filePath, sourceCode);
|
|
235
|
+
if (!ast) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const localContourIds = collectContourDefinitionIds(ast);
|
|
240
|
+
return checkContourDeclarations(
|
|
241
|
+
ast,
|
|
242
|
+
sourceCode,
|
|
243
|
+
filePath,
|
|
244
|
+
mergeKnownContourIds(localContourIds, context.knownContourIds)
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
description:
|
|
248
|
+
'Ensure every contour declared on a trail resolves to a known contour definition.',
|
|
249
|
+
name: 'contour-exists',
|
|
250
|
+
severity: 'error',
|
|
251
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge a file's locally-defined contour IDs with the project-wide set.
|
|
3
|
+
*
|
|
4
|
+
* Rules that run with a `ProjectContext` need to treat both local and
|
|
5
|
+
* project-wide contour 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 mergeKnownContourIds = (
|
|
10
|
+
localContourIds: ReadonlySet<string>,
|
|
11
|
+
projectContourIds?: ReadonlySet<string>
|
|
12
|
+
): ReadonlySet<string> =>
|
|
13
|
+
projectContourIds
|
|
14
|
+
? new Set([...projectContourIds, ...localContourIds])
|
|
15
|
+
: localContourIds;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectComposeTargetTrailIds,
|
|
3
|
+
findConfigProperty,
|
|
4
|
+
findTrailDefinitions,
|
|
5
|
+
getStringValue,
|
|
6
|
+
isStringLiteral,
|
|
7
|
+
offsetToLine,
|
|
8
|
+
parse,
|
|
9
|
+
} from './ast.js';
|
|
10
|
+
import type { AstNode } from './ast.js';
|
|
11
|
+
import { isTestFile } from './scan.js';
|
|
12
|
+
import type {
|
|
13
|
+
ProjectAwareWardenRule,
|
|
14
|
+
ProjectContext,
|
|
15
|
+
WardenDiagnostic,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
const isNonEmptyActivationValue = (onValue: AstNode): boolean => {
|
|
19
|
+
// Identifier reference (e.g. `on: signalsArray`) — conservatively treat as
|
|
20
|
+
// having activation to avoid false positives. We can't cheaply resolve what
|
|
21
|
+
// the identifier binds to, so assume it's a non-empty activation.
|
|
22
|
+
if (onValue.type === 'Identifier') {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (onValue.type !== 'ArrayExpression') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const elements = onValue['elements'] as readonly AstNode[] | undefined;
|
|
29
|
+
return (elements?.length ?? 0) > 0;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const hasOnActivation = (config: AstNode): boolean => {
|
|
33
|
+
const onProp = findConfigProperty(config, 'on');
|
|
34
|
+
const onValue = onProp?.value as AstNode | undefined;
|
|
35
|
+
return onValue ? isNonEmptyActivationValue(onValue) : false;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const hasExplicitInternalVisibility = (config: AstNode): boolean => {
|
|
39
|
+
const visibilityProp = findConfigProperty(config, 'visibility');
|
|
40
|
+
const visibilityValue = visibilityProp?.value as AstNode | undefined;
|
|
41
|
+
return (
|
|
42
|
+
!!visibilityValue &&
|
|
43
|
+
isStringLiteral(visibilityValue) &&
|
|
44
|
+
getStringValue(visibilityValue) === 'internal'
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Check legacy `meta: { internal: true }` convention (mirrors runtime effectiveVisibility). */
|
|
49
|
+
const hasLegacyMetaInternal = (config: AstNode): boolean => {
|
|
50
|
+
const metaProp = findConfigProperty(config, 'meta');
|
|
51
|
+
const metaValue = metaProp?.value as AstNode | undefined;
|
|
52
|
+
if (!metaValue || metaValue.type !== 'ObjectExpression') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const internalProp = findConfigProperty(metaValue, 'internal');
|
|
56
|
+
const internalValue = internalProp?.value as AstNode | undefined;
|
|
57
|
+
return (
|
|
58
|
+
internalValue?.type === 'BooleanLiteral' &&
|
|
59
|
+
(internalValue as unknown as { value: boolean }).value === true
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isInternalTrail = (config: AstNode): boolean =>
|
|
64
|
+
hasExplicitInternalVisibility(config) || hasLegacyMetaInternal(config);
|
|
65
|
+
|
|
66
|
+
const buildDeadInternalTrailDiagnostic = (
|
|
67
|
+
trailId: string,
|
|
68
|
+
filePath: string,
|
|
69
|
+
line: number
|
|
70
|
+
): WardenDiagnostic => ({
|
|
71
|
+
filePath,
|
|
72
|
+
line,
|
|
73
|
+
message: `Trail "${trailId}" is marked visibility: 'internal' but nothing composes it and it has no on: activation. Internal trails should stay reachable through ctx.compose() or reactive activation.`,
|
|
74
|
+
rule: 'dead-internal-trail',
|
|
75
|
+
severity: 'warn',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const checkDeadInternalTrails = (
|
|
79
|
+
ast: AstNode | null,
|
|
80
|
+
sourceCode: string,
|
|
81
|
+
filePath: string,
|
|
82
|
+
composedTrailIds: ReadonlySet<string>
|
|
83
|
+
): readonly WardenDiagnostic[] => {
|
|
84
|
+
if (isTestFile(filePath) || !ast) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
89
|
+
|
|
90
|
+
for (const def of findTrailDefinitions(ast)) {
|
|
91
|
+
if (def.kind !== 'trail' || !isInternalTrail(def.config)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasOnActivation(def.config) || composedTrailIds.has(def.id)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
diagnostics.push(
|
|
100
|
+
buildDeadInternalTrailDiagnostic(
|
|
101
|
+
def.id,
|
|
102
|
+
filePath,
|
|
103
|
+
offsetToLine(sourceCode, def.start)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return diagnostics;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const deadInternalTrail: ProjectAwareWardenRule = {
|
|
112
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
113
|
+
const ast = parse(filePath, sourceCode);
|
|
114
|
+
return checkDeadInternalTrails(
|
|
115
|
+
ast,
|
|
116
|
+
sourceCode,
|
|
117
|
+
filePath,
|
|
118
|
+
ast ? collectComposeTargetTrailIds(ast, sourceCode) : new Set<string>()
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
checkWithContext(
|
|
122
|
+
sourceCode: string,
|
|
123
|
+
filePath: string,
|
|
124
|
+
context: ProjectContext
|
|
125
|
+
): readonly WardenDiagnostic[] {
|
|
126
|
+
const ast = parse(filePath, sourceCode);
|
|
127
|
+
const localComposeTargetTrailIds = ast
|
|
128
|
+
? collectComposeTargetTrailIds(ast, sourceCode)
|
|
129
|
+
: new Set<string>();
|
|
130
|
+
// Union project-wide compose evidence with the file-local evidence rather
|
|
131
|
+
// than preferring one over the other. The project context only collects
|
|
132
|
+
// compose edges from registered app topos, so a trail defined in a package
|
|
133
|
+
// that is scanned but not part of any registered topo (e.g. an internal
|
|
134
|
+
// child composed in its own module) would be absent from the context set
|
|
135
|
+
// yet present in the local set. Preferring the context set alone produced a
|
|
136
|
+
// false dead-internal-trail warning for those same-file compositions.
|
|
137
|
+
const composeTargetTrailIds = context.composeTargetTrailIds
|
|
138
|
+
? new Set<string>([
|
|
139
|
+
...context.composeTargetTrailIds,
|
|
140
|
+
...localComposeTargetTrailIds,
|
|
141
|
+
])
|
|
142
|
+
: localComposeTargetTrailIds;
|
|
143
|
+
return checkDeadInternalTrails(
|
|
144
|
+
ast,
|
|
145
|
+
sourceCode,
|
|
146
|
+
filePath,
|
|
147
|
+
composeTargetTrailIds
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
description:
|
|
151
|
+
'Warn when an internal trail has no compositions anywhere in the project and no on: activation.',
|
|
152
|
+
name: 'dead-internal-trail',
|
|
153
|
+
severity: 'warn',
|
|
154
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { isDraftId } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import { isDraftMarkedFile } from '../draft.js';
|
|
4
|
+
import {
|
|
5
|
+
collectFrameworkDraftPrefixConstantOffsets,
|
|
6
|
+
findStringLiterals,
|
|
7
|
+
hasIgnoreCommentOnLine,
|
|
8
|
+
offsetToLine,
|
|
9
|
+
parse,
|
|
10
|
+
splitSourceLines,
|
|
11
|
+
} from './ast.js';
|
|
12
|
+
import type { StringLiteralMatch } from './ast.js';
|
|
13
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
14
|
+
|
|
15
|
+
const messageForMissingMarker = (draftId: string): string =>
|
|
16
|
+
`Draft id "${draftId}" appears in source, but the file is not draft-marked. ` +
|
|
17
|
+
'Rename it with an _draft. prefix or a .draft. trailing segment.';
|
|
18
|
+
|
|
19
|
+
const makeDiagnostic = (
|
|
20
|
+
sourceCode: string,
|
|
21
|
+
filePath: string,
|
|
22
|
+
start: number,
|
|
23
|
+
message: string,
|
|
24
|
+
severity: WardenDiagnostic['severity']
|
|
25
|
+
): WardenDiagnostic => ({
|
|
26
|
+
filePath,
|
|
27
|
+
line: offsetToLine(sourceCode, start),
|
|
28
|
+
message,
|
|
29
|
+
rule: 'draft-file-marking',
|
|
30
|
+
severity,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const collectDraftMatches = (
|
|
34
|
+
sourceCode: string,
|
|
35
|
+
filePath: string,
|
|
36
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
37
|
+
): StringLiteralMatch[] => {
|
|
38
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
39
|
+
ast,
|
|
40
|
+
filePath
|
|
41
|
+
);
|
|
42
|
+
const lines = splitSourceLines(sourceCode);
|
|
43
|
+
return findStringLiterals(ast, (value) => isDraftId(value)).filter(
|
|
44
|
+
(match) => {
|
|
45
|
+
if (frameworkConstantOffsets.has(match.start)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start))
|
|
50
|
+
) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const draftMissingMarkerDiagnostic = (
|
|
59
|
+
sourceCode: string,
|
|
60
|
+
filePath: string,
|
|
61
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
62
|
+
): WardenDiagnostic | null => {
|
|
63
|
+
const draftMatches = collectDraftMatches(sourceCode, filePath, ast);
|
|
64
|
+
if (!draftMatches.length || isDraftMarkedFile(filePath)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [first] = draftMatches;
|
|
69
|
+
if (!first) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return makeDiagnostic(
|
|
74
|
+
sourceCode,
|
|
75
|
+
filePath,
|
|
76
|
+
first.start,
|
|
77
|
+
messageForMissingMarker(first.value),
|
|
78
|
+
'error'
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const draftMarkedWithoutIdsDiagnostic = (
|
|
83
|
+
filePath: string,
|
|
84
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
85
|
+
): WardenDiagnostic | null => {
|
|
86
|
+
// Deciding whether the file's `_draft.` marker is still warranted is a
|
|
87
|
+
// question about *all* draft ids present in source, not just the unsuppressed
|
|
88
|
+
// ones. Pragma-suppressed ids still justify a draft-marked filename — a user
|
|
89
|
+
// intentionally silencing them has not removed the draft content. We
|
|
90
|
+
// therefore filter only the framework-constant declarations (which are not
|
|
91
|
+
// draft ids at all) and bypass the pragma filter that `collectDraftMatches`
|
|
92
|
+
// applies.
|
|
93
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
94
|
+
ast,
|
|
95
|
+
filePath
|
|
96
|
+
);
|
|
97
|
+
const unsuppressedDraftIds = findStringLiterals(ast, (value) =>
|
|
98
|
+
isDraftId(value)
|
|
99
|
+
).filter((match) => !frameworkConstantOffsets.has(match.start));
|
|
100
|
+
|
|
101
|
+
if (unsuppressedDraftIds.length > 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isDraftMarkedFile(filePath)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
filePath,
|
|
111
|
+
line: 1,
|
|
112
|
+
message:
|
|
113
|
+
'File is draft-marked but no longer contains draft ids. Remove the draft filename marker or finish the promotion cleanup.',
|
|
114
|
+
rule: 'draft-file-marking',
|
|
115
|
+
severity: 'warn',
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const collectDraftFileMarkingDiagnostics = (
|
|
120
|
+
sourceCode: string,
|
|
121
|
+
filePath: string,
|
|
122
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
123
|
+
): WardenDiagnostic[] => {
|
|
124
|
+
const missingMarkerDiagnostic = draftMissingMarkerDiagnostic(
|
|
125
|
+
sourceCode,
|
|
126
|
+
filePath,
|
|
127
|
+
ast
|
|
128
|
+
);
|
|
129
|
+
if (missingMarkerDiagnostic) {
|
|
130
|
+
return [missingMarkerDiagnostic];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const markedWithoutIdsDiagnostic = draftMarkedWithoutIdsDiagnostic(
|
|
134
|
+
filePath,
|
|
135
|
+
ast
|
|
136
|
+
);
|
|
137
|
+
if (markedWithoutIdsDiagnostic) {
|
|
138
|
+
return [markedWithoutIdsDiagnostic];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ensures files containing draft ids are visibly marked as draft-bearing files.
|
|
146
|
+
*/
|
|
147
|
+
export const draftFileMarking: WardenRule = {
|
|
148
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
149
|
+
const ast = parse(filePath, sourceCode);
|
|
150
|
+
if (!ast) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return collectDraftFileMarkingDiagnostics(sourceCode, filePath, ast);
|
|
155
|
+
},
|
|
156
|
+
description:
|
|
157
|
+
'Require draft-bearing files to use _draft.* or *.draft.* filename markers.',
|
|
158
|
+
name: 'draft-file-marking',
|
|
159
|
+
severity: 'error',
|
|
160
|
+
};
|