@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.22
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 +508 -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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isDraftId } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
collectFrameworkDraftPrefixConstantOffsets,
|
|
5
|
+
findStringLiterals,
|
|
6
|
+
hasIgnoreCommentOnLine,
|
|
7
|
+
offsetToLine,
|
|
8
|
+
parse,
|
|
9
|
+
splitSourceLines,
|
|
10
|
+
} from './ast.js';
|
|
11
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
12
|
+
|
|
13
|
+
const createDiagnostic = (
|
|
14
|
+
sourceCode: string,
|
|
15
|
+
filePath: string,
|
|
16
|
+
match: { start: number; value: string }
|
|
17
|
+
): WardenDiagnostic => ({
|
|
18
|
+
filePath,
|
|
19
|
+
line: offsetToLine(sourceCode, match.start),
|
|
20
|
+
message:
|
|
21
|
+
`Draft id "${match.value}" is still visible debt. ` +
|
|
22
|
+
'Established surfaces, lock export, and OpenAPI generation will reject it until it is promoted.',
|
|
23
|
+
rule: 'draft-visible-debt',
|
|
24
|
+
severity: 'warn',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const isSuppressedMatch = (
|
|
28
|
+
match: { start: number },
|
|
29
|
+
sourceCode: string,
|
|
30
|
+
lines: readonly string[],
|
|
31
|
+
frameworkConstantOffsets: ReadonlySet<number>
|
|
32
|
+
): boolean =>
|
|
33
|
+
frameworkConstantOffsets.has(match.start) ||
|
|
34
|
+
hasIgnoreCommentOnLine(lines, offsetToLine(sourceCode, match.start));
|
|
35
|
+
|
|
36
|
+
const collectDraftVisibleDebtDiagnostics = (
|
|
37
|
+
sourceCode: string,
|
|
38
|
+
filePath: string,
|
|
39
|
+
ast: NonNullable<ReturnType<typeof parse>>
|
|
40
|
+
): WardenDiagnostic[] => {
|
|
41
|
+
const frameworkConstantOffsets = collectFrameworkDraftPrefixConstantOffsets(
|
|
42
|
+
ast,
|
|
43
|
+
filePath
|
|
44
|
+
);
|
|
45
|
+
const lines = splitSourceLines(sourceCode);
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
|
|
48
|
+
return findStringLiterals(ast, (value) => isDraftId(value)).flatMap(
|
|
49
|
+
(match) => {
|
|
50
|
+
if (
|
|
51
|
+
isSuppressedMatch(match, sourceCode, lines, frameworkConstantOffsets)
|
|
52
|
+
) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const key = `${match.value}:${String(match.start)}`;
|
|
56
|
+
if (seen.has(key)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
seen.add(key);
|
|
60
|
+
return [createDiagnostic(sourceCode, filePath, match)];
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Warns when draft ids are still present so the debt stays visible during
|
|
67
|
+
* review even when the file is correctly marked.
|
|
68
|
+
*
|
|
69
|
+
* Severity is intentionally `warn`, not `error`. The hard rejection layer for
|
|
70
|
+
* draft state leaking into established outputs is `validateEstablishedTopo` at
|
|
71
|
+
* runtime — it blocks topo compile, surface projection, and lockfile writes.
|
|
72
|
+
* This rule surfaces the debt for human reviewers without duplicating that layer.
|
|
73
|
+
*/
|
|
74
|
+
export const draftVisibleDebt: WardenRule = {
|
|
75
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
76
|
+
const ast = parse(filePath, sourceCode);
|
|
77
|
+
if (!ast) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return collectDraftVisibleDebtDiagnostics(sourceCode, filePath, ast);
|
|
82
|
+
},
|
|
83
|
+
description:
|
|
84
|
+
'Warn when draft ids remain in source so the debt stays visible during review.',
|
|
85
|
+
name: 'draft-visible-debt',
|
|
86
|
+
severity: 'warn',
|
|
87
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
getStringValue,
|
|
14
|
+
identifierName,
|
|
15
|
+
isStringLiteral,
|
|
16
|
+
offsetToLine,
|
|
17
|
+
parse,
|
|
18
|
+
walk,
|
|
19
|
+
} from './ast.js';
|
|
20
|
+
import type { AstNode } from './ast.js';
|
|
21
|
+
import { isTestFile } from './scan.js';
|
|
22
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
23
|
+
|
|
24
|
+
const MEMBER_EXPRESSION_TYPES = new Set([
|
|
25
|
+
'MemberExpression',
|
|
26
|
+
'StaticMemberExpression',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const MAPPER_FACTORY_NAMES = new Set(['createSurfaceErrorMapper']);
|
|
30
|
+
|
|
31
|
+
const mappedErrorClassCategories = new Set(
|
|
32
|
+
errorClasses.flatMap((entry) =>
|
|
33
|
+
entry.category === 'dynamic' ? [] : [entry.category]
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const requiredErrorCategories = (
|
|
38
|
+
Object.keys(codesByCategory) as ErrorCategory[]
|
|
39
|
+
).filter((category) => mappedErrorClassCategories.has(category));
|
|
40
|
+
|
|
41
|
+
const getPropertyName = (node: AstNode | undefined): string | null => {
|
|
42
|
+
if (!node) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
identifierName(node) ??
|
|
48
|
+
(isStringLiteral(node) ? getStringValue(node) : null)
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const collectObjectBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
|
|
53
|
+
const bindings = new Map<string, AstNode>();
|
|
54
|
+
|
|
55
|
+
walk(ast, (node) => {
|
|
56
|
+
if (node.type !== 'VariableDeclarator') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { id, init } = node as { id?: AstNode; init?: AstNode };
|
|
61
|
+
const bindingName = identifierName(id);
|
|
62
|
+
|
|
63
|
+
if (bindingName && init?.type === 'ObjectExpression') {
|
|
64
|
+
bindings.set(bindingName, init);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return bindings;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getObjectProperties = (objectNode: AstNode): readonly AstNode[] =>
|
|
72
|
+
objectNode.type === 'ObjectExpression'
|
|
73
|
+
? ((objectNode['properties'] as readonly AstNode[] | undefined) ?? [])
|
|
74
|
+
: [];
|
|
75
|
+
|
|
76
|
+
const findObjectPropertyValue = (
|
|
77
|
+
objectNode: AstNode,
|
|
78
|
+
propertyName: string
|
|
79
|
+
): AstNode | null => {
|
|
80
|
+
for (const property of getObjectProperties(objectNode)) {
|
|
81
|
+
if (property.type !== 'Property') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = getPropertyName((property as unknown as { key?: AstNode }).key);
|
|
86
|
+
if (key === propertyName) {
|
|
87
|
+
return (property as unknown as { value?: AstNode }).value ?? null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolveIdentifierObject = (
|
|
95
|
+
node: AstNode,
|
|
96
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
97
|
+
): AstNode | null =>
|
|
98
|
+
bindings.get((node as { name?: string }).name ?? '') ?? null;
|
|
99
|
+
|
|
100
|
+
const resolveMemberObject = (
|
|
101
|
+
node: AstNode,
|
|
102
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
103
|
+
depth: number,
|
|
104
|
+
resolve: (
|
|
105
|
+
node: AstNode | undefined,
|
|
106
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
107
|
+
depth?: number
|
|
108
|
+
) => AstNode | null
|
|
109
|
+
): AstNode | null => {
|
|
110
|
+
const { object, property } = node as { object?: AstNode; property?: AstNode };
|
|
111
|
+
const propertyName = getPropertyName(property);
|
|
112
|
+
if (!propertyName) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const objectNode = resolve(object, bindings, depth + 1);
|
|
117
|
+
return objectNode
|
|
118
|
+
? resolve(
|
|
119
|
+
findObjectPropertyValue(objectNode, propertyName) ?? undefined,
|
|
120
|
+
bindings,
|
|
121
|
+
depth + 1
|
|
122
|
+
)
|
|
123
|
+
: null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const resolveObjectExpression = function resolveObjectExpression(
|
|
127
|
+
node: AstNode | undefined,
|
|
128
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
129
|
+
depth = 0
|
|
130
|
+
): AstNode | null {
|
|
131
|
+
if (!node || depth > 4) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (node.type === 'ObjectExpression') {
|
|
136
|
+
return node;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (node.type === 'Identifier') {
|
|
140
|
+
return resolveIdentifierObject(node, bindings);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return MEMBER_EXPRESSION_TYPES.has(node.type)
|
|
144
|
+
? resolveMemberObject(node, bindings, depth, resolveObjectExpression)
|
|
145
|
+
: null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const addMappedCategory = (
|
|
149
|
+
categories: Set<string>,
|
|
150
|
+
property: AstNode
|
|
151
|
+
): boolean => {
|
|
152
|
+
if (property.type === 'SpreadElement') {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (property.type !== 'Property') {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const key = getPropertyName((property as unknown as { key?: AstNode }).key);
|
|
161
|
+
if (key) {
|
|
162
|
+
categories.add(key);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const collectMappedCategories = (
|
|
169
|
+
mapperObject: AstNode
|
|
170
|
+
): ReadonlySet<string> | null => {
|
|
171
|
+
if (mapperObject.type !== 'ObjectExpression') {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const categories = new Set<string>();
|
|
176
|
+
for (const property of getObjectProperties(mapperObject)) {
|
|
177
|
+
if (!addMappedCategory(categories, property)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return categories;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const createDiagnostic = (
|
|
186
|
+
filePath: string,
|
|
187
|
+
line: number,
|
|
188
|
+
missingCategories: readonly string[]
|
|
189
|
+
): WardenDiagnostic => ({
|
|
190
|
+
filePath,
|
|
191
|
+
line,
|
|
192
|
+
message: `Surface error mapper is missing mappings for: ${missingCategories.join(', ')}. Registered createSurfaceErrorMapper() calls must cover every ErrorCategory.`,
|
|
193
|
+
rule: 'error-mapping-completeness',
|
|
194
|
+
severity: 'error',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const getCallArgs = (node: AstNode): readonly AstNode[] =>
|
|
198
|
+
(node as { arguments?: readonly AstNode[] }).arguments ?? [];
|
|
199
|
+
|
|
200
|
+
const getCallCallee = (node: AstNode): AstNode | undefined =>
|
|
201
|
+
(node as { callee?: AstNode }).callee;
|
|
202
|
+
|
|
203
|
+
const isMapperFactoryCall = (node: AstNode): boolean =>
|
|
204
|
+
node.type === 'CallExpression' &&
|
|
205
|
+
MAPPER_FACTORY_NAMES.has(identifierName(getCallCallee(node)) ?? '');
|
|
206
|
+
|
|
207
|
+
const findMissingCategories = (
|
|
208
|
+
mappedCategories: ReadonlySet<string>
|
|
209
|
+
): readonly string[] =>
|
|
210
|
+
requiredErrorCategories.filter((category) => !mappedCategories.has(category));
|
|
211
|
+
|
|
212
|
+
const resolveMappedCategories = (
|
|
213
|
+
node: AstNode,
|
|
214
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
215
|
+
): ReadonlySet<string> | null => {
|
|
216
|
+
const [firstArg] = getCallArgs(node);
|
|
217
|
+
const mapperObject = resolveObjectExpression(firstArg, bindings);
|
|
218
|
+
return mapperObject ? collectMappedCategories(mapperObject) : null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const inspectMapperCall = (
|
|
222
|
+
node: AstNode,
|
|
223
|
+
bindings: ReadonlyMap<string, AstNode>,
|
|
224
|
+
filePath: string,
|
|
225
|
+
sourceCode: string
|
|
226
|
+
): WardenDiagnostic | null => {
|
|
227
|
+
if (!isMapperFactoryCall(node)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const mappedCategories = resolveMappedCategories(node, bindings);
|
|
232
|
+
if (!mappedCategories) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const missingCategories = findMissingCategories(mappedCategories);
|
|
237
|
+
if (missingCategories.length === 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return createDiagnostic(
|
|
242
|
+
filePath,
|
|
243
|
+
offsetToLine(sourceCode, node.start),
|
|
244
|
+
missingCategories
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Flags registered surface error mapper calls that omit error categories.
|
|
250
|
+
*/
|
|
251
|
+
export const errorMappingCompleteness: WardenRule = {
|
|
252
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
253
|
+
if (
|
|
254
|
+
isTestFile(filePath) ||
|
|
255
|
+
![...MAPPER_FACTORY_NAMES].some((factoryName) =>
|
|
256
|
+
sourceCode.includes(factoryName)
|
|
257
|
+
)
|
|
258
|
+
) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const ast = parse(filePath, sourceCode);
|
|
263
|
+
if (!ast) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const bindings = collectObjectBindings(ast);
|
|
268
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
269
|
+
|
|
270
|
+
walk(ast, (node) => {
|
|
271
|
+
const diagnostic = inspectMapperCall(
|
|
272
|
+
node,
|
|
273
|
+
bindings,
|
|
274
|
+
filePath,
|
|
275
|
+
sourceCode
|
|
276
|
+
);
|
|
277
|
+
if (diagnostic) {
|
|
278
|
+
diagnostics.push(diagnostic);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return diagnostics;
|
|
283
|
+
},
|
|
284
|
+
description:
|
|
285
|
+
'Require registered surface error mappers to cover every ErrorCategory.',
|
|
286
|
+
name: 'error-mapping-completeness',
|
|
287
|
+
severity: 'error',
|
|
288
|
+
};
|