@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,618 @@
|
|
|
1
|
+
/** Warden-private entity reference and user-namespace collectors. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildFrameworkNamespaceContext,
|
|
5
|
+
extractEntityDefinition,
|
|
6
|
+
extractStringLiteral,
|
|
7
|
+
findEntityDefinitions,
|
|
8
|
+
getImportSourceValue,
|
|
9
|
+
getPropertyName,
|
|
10
|
+
identifierName,
|
|
11
|
+
isFrameworkNamespaceSource,
|
|
12
|
+
isMemberAccessNonComputed,
|
|
13
|
+
isShadowed,
|
|
14
|
+
walk,
|
|
15
|
+
walkWithScopes,
|
|
16
|
+
} from '@ontrails/source';
|
|
17
|
+
import type {
|
|
18
|
+
AstNode,
|
|
19
|
+
EntityDefinition,
|
|
20
|
+
FrameworkNamespaceContext,
|
|
21
|
+
} from '@ontrails/source';
|
|
22
|
+
|
|
23
|
+
export const collectEntityDefinitionIds = (ast: AstNode): ReadonlySet<string> =>
|
|
24
|
+
new Set(findEntityDefinitions(ast).map((def) => def.name));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Collect the `localBinding → entityName` map for `const foo = entity(...)`
|
|
28
|
+
* declarations. Inline entity calls are intentionally excluded because they
|
|
29
|
+
* have no local binding — use {@link collectEntityDefinitionIds} when the
|
|
30
|
+
* full set of declared names is required.
|
|
31
|
+
*/
|
|
32
|
+
export const collectNamedEntityIds = (
|
|
33
|
+
ast: AstNode
|
|
34
|
+
): ReadonlyMap<string, string> => {
|
|
35
|
+
const ids = new Map<string, string>();
|
|
36
|
+
|
|
37
|
+
for (const def of findEntityDefinitions(ast)) {
|
|
38
|
+
if (def.bindingName) {
|
|
39
|
+
ids.set(def.bindingName, def.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ids;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const resolveNamedImportedName = (
|
|
47
|
+
specifier: AstNode,
|
|
48
|
+
localName: string
|
|
49
|
+
): string => {
|
|
50
|
+
const { imported } = specifier as unknown as { imported?: AstNode };
|
|
51
|
+
const importedName = imported
|
|
52
|
+
? (identifierName(imported) ?? extractStringLiteral(imported))
|
|
53
|
+
: null;
|
|
54
|
+
return importedName ?? localName;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const extractImportSpecifierAlias = (
|
|
58
|
+
specifier: AstNode
|
|
59
|
+
): { readonly localName: string; readonly importedName: string } | null => {
|
|
60
|
+
if (
|
|
61
|
+
specifier.type !== 'ImportSpecifier' &&
|
|
62
|
+
specifier.type !== 'ImportDefaultSpecifier'
|
|
63
|
+
) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { local } = specifier as unknown as { local?: AstNode };
|
|
68
|
+
const localName = identifierName(local);
|
|
69
|
+
if (!localName) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Default imports bind the default export of the source module to the local
|
|
74
|
+
// name. We cannot statically recover the exported name without compose-file
|
|
75
|
+
// analysis, so the local name is the best identifier we have for resolving
|
|
76
|
+
// against `knownEntityIds`. Treat the alias as an identity mapping; the
|
|
77
|
+
// downstream resolver will fall through to `knownEntityIds` on the binding
|
|
78
|
+
// name and report it as missing when not found.
|
|
79
|
+
if (specifier.type === 'ImportDefaultSpecifier') {
|
|
80
|
+
return { importedName: localName, localName };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
importedName: resolveNamedImportedName(specifier, localName),
|
|
85
|
+
localName,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Collect `import {
|
|
91
|
+
foo as bar
|
|
92
|
+
} from '...';` and `import bar from '...'`
|
|
93
|
+
* specifier mappings keyed by local binding name. The value is the original
|
|
94
|
+
* exported name for named imports. Default imports map to themselves because
|
|
95
|
+
* the exported name cannot be recovered statically — callers should fall
|
|
96
|
+
* through to `knownEntityIds` membership on the local binding name.
|
|
97
|
+
*/
|
|
98
|
+
export const collectImportAliasMap = (
|
|
99
|
+
ast: AstNode
|
|
100
|
+
): ReadonlyMap<string, string> => {
|
|
101
|
+
const aliases = new Map<string, string>();
|
|
102
|
+
|
|
103
|
+
walk(ast, (node) => {
|
|
104
|
+
if (node.type !== 'ImportDeclaration') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const specifiers =
|
|
109
|
+
(node['specifiers'] as readonly AstNode[] | undefined) ?? [];
|
|
110
|
+
for (const specifier of specifiers) {
|
|
111
|
+
const alias = extractImportSpecifierAlias(specifier);
|
|
112
|
+
if (alias) {
|
|
113
|
+
aliases.set(alias.localName, alias.importedName);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return aliases;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const addUserNamespaceBindingsFromDeclaration = (
|
|
122
|
+
node: AstNode,
|
|
123
|
+
into: Set<string>
|
|
124
|
+
): void => {
|
|
125
|
+
if (isFrameworkNamespaceSource(getImportSourceValue(node))) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const specifiers =
|
|
129
|
+
(node['specifiers'] as readonly AstNode[] | undefined) ?? [];
|
|
130
|
+
for (const specifier of specifiers) {
|
|
131
|
+
if (specifier.type !== 'ImportNamespaceSpecifier') {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const { local } = specifier as unknown as { local?: AstNode };
|
|
135
|
+
const localName = identifierName(local);
|
|
136
|
+
if (localName) {
|
|
137
|
+
into.add(localName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Collect local binding names introduced by `import * as <name> from '<src>'`
|
|
144
|
+
* declarations whose source is NOT an `@ontrails/*` framework package. These
|
|
145
|
+
* are user-defined namespace imports of entity modules (e.g. `import * as
|
|
146
|
+
* entities from './entities'`), used to resolve `entities.user` member-access
|
|
147
|
+
* references to entity ids.
|
|
148
|
+
*
|
|
149
|
+
* Framework namespace imports (`import * as core from '@ontrails/core'`) are
|
|
150
|
+
* intentionally excluded — they carry framework primitives like
|
|
151
|
+
* `core.entity(...)` and are resolved by {@link buildFrameworkNamespaceContext}.
|
|
152
|
+
* Mixing them here would treat `core.entity` as a reference to an entity
|
|
153
|
+
* named "entity", producing false positives.
|
|
154
|
+
*/
|
|
155
|
+
export const collectUserNamespaceImportBindings = (
|
|
156
|
+
ast: AstNode
|
|
157
|
+
): ReadonlySet<string> => {
|
|
158
|
+
const bindings = new Set<string>();
|
|
159
|
+
|
|
160
|
+
walk(ast, (node) => {
|
|
161
|
+
if (node.type !== 'ImportDeclaration') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
addUserNamespaceBindingsFromDeclaration(node, bindings);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return bindings;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolution context for user-namespace member access like `entities.user`.
|
|
172
|
+
* Bundles the set of local namespace-binding names (from `import * as x from
|
|
173
|
+
* './entities'`) with an optional set of proven-safe `MemberExpression` start
|
|
174
|
+
* offsets from a scope-aware pre-pass. When `safeMemberStarts` is present, a
|
|
175
|
+
* member access only resolves to a user-namespace target if its start is in
|
|
176
|
+
* the set — so a function-local shadow of the namespace import does not leak
|
|
177
|
+
* through. When absent, the name-only gate is used as a
|
|
178
|
+
* backward-compatible fallback for ad-hoc callers.
|
|
179
|
+
*/
|
|
180
|
+
export interface UserNamespaceContext {
|
|
181
|
+
readonly bindings: ReadonlySet<string>;
|
|
182
|
+
readonly safeMemberStarts?: ReadonlySet<number>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Walk the AST with a scope stack and collect `MemberExpression` start offsets
|
|
187
|
+
* whose receiver is a user-namespace binding that is NOT shadowed by any
|
|
188
|
+
* enclosing scope. Mirrors `collectFrameworkNamespacedCallStarts` for the
|
|
189
|
+
* framework-namespace path so `entities.user` inside
|
|
190
|
+
* `function f(entities) { ... }` is rejected as shadowed.
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* Return the receiver-identifier name of a non-computed member access, or
|
|
194
|
+
* `null` for any other node shape (computed access, non-member, etc.).
|
|
195
|
+
*/
|
|
196
|
+
const getNonComputedMemberReceiver = (node: AstNode): string | null => {
|
|
197
|
+
if (!isMemberAccessNonComputed(node)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const { object } = node as unknown as { object?: AstNode };
|
|
201
|
+
return object ? identifierName(object) : null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const collectUserNamespacedMemberStarts = (
|
|
205
|
+
ast: AstNode,
|
|
206
|
+
bindings: ReadonlySet<string>
|
|
207
|
+
): ReadonlySet<number> => {
|
|
208
|
+
const starts = new Set<number>();
|
|
209
|
+
if (bindings.size === 0) {
|
|
210
|
+
return starts;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
walkWithScopes(ast, (node, scopes) => {
|
|
214
|
+
const receiver = getNonComputedMemberReceiver(node);
|
|
215
|
+
if (!receiver || !bindings.has(receiver) || isShadowed(receiver, scopes)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
starts.add(node.start);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return starts;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Build a {@link UserNamespaceContext} for `ast`, including the scope-aware
|
|
226
|
+
* `safeMemberStarts` gate. Prefer this over bare
|
|
227
|
+
* {@link collectUserNamespaceImportBindings} so member access like
|
|
228
|
+
* `entities.user` is rejected when `entities` is shadowed by a local binding.
|
|
229
|
+
*/
|
|
230
|
+
export const buildUserNamespaceContext = (
|
|
231
|
+
ast: AstNode
|
|
232
|
+
): UserNamespaceContext => {
|
|
233
|
+
const bindings = collectUserNamespaceImportBindings(ast);
|
|
234
|
+
return {
|
|
235
|
+
bindings,
|
|
236
|
+
safeMemberStarts: collectUserNamespacedMemberStarts(ast, bindings),
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export interface EntityReferenceSite {
|
|
241
|
+
/** Field on the source entity that declares the reference. */
|
|
242
|
+
readonly field: string;
|
|
243
|
+
/** Source entity name. */
|
|
244
|
+
readonly source: string;
|
|
245
|
+
/** Start offset of the field declaration. */
|
|
246
|
+
readonly start: number;
|
|
247
|
+
/** Target entity name. */
|
|
248
|
+
readonly target: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const stripEntitySuffix = (name: string): string => {
|
|
252
|
+
const suffix = 'Entity';
|
|
253
|
+
return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const resolveKnownEntityName = (
|
|
257
|
+
name: string,
|
|
258
|
+
knownEntityIds?: ReadonlySet<string>
|
|
259
|
+
): string | null => {
|
|
260
|
+
if (knownEntityIds?.has(name)) {
|
|
261
|
+
return name;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Support the common `const userEntity = entity('user', ...)` naming
|
|
265
|
+
// pattern when callers refer to the binding name instead of the entity ID.
|
|
266
|
+
// Exact matches always win; suffix stripping is a fallback only.
|
|
267
|
+
const stripped = stripEntitySuffix(name);
|
|
268
|
+
if (stripped !== name && knownEntityIds?.has(stripped)) {
|
|
269
|
+
return stripped;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Resolve a local binding name to an entity ID, honoring import aliases.
|
|
277
|
+
*
|
|
278
|
+
* Strategies, in order:
|
|
279
|
+
* 1. Local `const foo = entity('name', ...)` binding → the entity name.
|
|
280
|
+
* 2. `knownEntityIds` membership on the binding name itself (or the
|
|
281
|
+
* conventional `Entity` suffix strip).
|
|
282
|
+
* 3. `import { foo as bar }` → use the original exported name `foo`
|
|
283
|
+
* (and apply strategy 2 / suffix-stripping against it so aliased imports
|
|
284
|
+
* resolve correctly). If the imported name still isn't recognized, the
|
|
285
|
+
* imported name is returned so the caller can report it missing.
|
|
286
|
+
*
|
|
287
|
+
* Returns `null` only when the name belongs to no known resolution path —
|
|
288
|
+
* no local binding, no known entity ID, no import, and no suffix match.
|
|
289
|
+
* Returning `null` means "this identifier is not an entity reference we can
|
|
290
|
+
* reason about" (e.g. a bare undeclared variable), as opposed to
|
|
291
|
+
* "an entity reference whose target is missing".
|
|
292
|
+
*/
|
|
293
|
+
export const deriveEntityIdentifierName = (
|
|
294
|
+
bindingName: string,
|
|
295
|
+
namedEntityIds: ReadonlyMap<string, string>,
|
|
296
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
297
|
+
importAliases?: ReadonlyMap<string, string>
|
|
298
|
+
): string | null => {
|
|
299
|
+
const localName = namedEntityIds.get(bindingName);
|
|
300
|
+
if (localName) {
|
|
301
|
+
return localName;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const known = resolveKnownEntityName(bindingName, knownEntityIds);
|
|
305
|
+
if (known) {
|
|
306
|
+
return known;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// If the binding came from an import, use the original exported name as
|
|
310
|
+
// the resolution target. This lets `import { foo as bar }` resolve to
|
|
311
|
+
// the exported `foo` rather than the local alias `bar`. If the imported
|
|
312
|
+
// name still isn't recognized, return it so callers can report it as
|
|
313
|
+
// missing under its original name.
|
|
314
|
+
const importedName = importAliases?.get(bindingName);
|
|
315
|
+
if (importedName) {
|
|
316
|
+
return resolveKnownEntityName(importedName, knownEntityIds) ?? importedName;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const getEntityReferenceMember = (
|
|
323
|
+
node: AstNode
|
|
324
|
+
): {
|
|
325
|
+
readonly object?: AstNode;
|
|
326
|
+
readonly property?: AstNode;
|
|
327
|
+
readonly start: number;
|
|
328
|
+
} | null => {
|
|
329
|
+
if (
|
|
330
|
+
node.type !== 'MemberExpression' &&
|
|
331
|
+
node.type !== 'StaticMemberExpression'
|
|
332
|
+
) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return node as unknown as {
|
|
337
|
+
readonly object?: AstNode;
|
|
338
|
+
readonly property?: AstNode;
|
|
339
|
+
readonly start: number;
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const asUserNamespaceContext = (
|
|
344
|
+
input: ReadonlySet<string> | UserNamespaceContext | undefined
|
|
345
|
+
): UserNamespaceContext | undefined => {
|
|
346
|
+
if (!input) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
return input instanceof Set
|
|
350
|
+
? { bindings: input }
|
|
351
|
+
: (input as UserNamespaceContext);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolve a user-namespace member access like `entities.user` to its entity
|
|
356
|
+
* id. Returns the property name (e.g. `'user'`) when the receiver identifier
|
|
357
|
+
* is a known user-defined namespace binding AND — when the caller provides a
|
|
358
|
+
* {@link UserNamespaceContext} with `safeMemberStarts` — the member access
|
|
359
|
+
* site is in that set (i.e. the receiver is not shadowed by any enclosing
|
|
360
|
+
* scope). Otherwise returns `null`.
|
|
361
|
+
*
|
|
362
|
+
* The property name is taken as the entity id verbatim — we cannot statically
|
|
363
|
+
* resolve what `entities.user` binds to without reading the other file, so we
|
|
364
|
+
* treat the member name as the candidate target and let
|
|
365
|
+
* {@link deriveEntityIdentifierName}'s downstream `knownEntityIds` check
|
|
366
|
+
* report a missing target.
|
|
367
|
+
*/
|
|
368
|
+
export const isUserNamespaceReceiverAllowed = (
|
|
369
|
+
receiver: string,
|
|
370
|
+
memberStart: number,
|
|
371
|
+
ctx: UserNamespaceContext
|
|
372
|
+
): boolean => {
|
|
373
|
+
if (!ctx.bindings.has(receiver)) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
// Scope-aware gate: when the pre-pass produced a set, the member access
|
|
377
|
+
// must appear in it. Without the set, fall back to the bare name check.
|
|
378
|
+
return ctx.safeMemberStarts ? ctx.safeMemberStarts.has(memberStart) : true;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const getEntityReferenceTargetFromNamespaceMember = (
|
|
382
|
+
member: {
|
|
383
|
+
readonly object?: AstNode;
|
|
384
|
+
readonly property?: AstNode;
|
|
385
|
+
readonly start: number;
|
|
386
|
+
},
|
|
387
|
+
userNamespace?: ReadonlySet<string> | UserNamespaceContext
|
|
388
|
+
): string | null => {
|
|
389
|
+
const ctx = asUserNamespaceContext(userNamespace);
|
|
390
|
+
if (!ctx || ctx.bindings.size === 0) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const receiver = member.object ? identifierName(member.object) : null;
|
|
394
|
+
if (
|
|
395
|
+
!receiver ||
|
|
396
|
+
!isUserNamespaceReceiverAllowed(receiver, member.start, ctx)
|
|
397
|
+
) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const { property } = member;
|
|
401
|
+
if (!property || property.type !== 'Identifier') {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return identifierName(property);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const getEntityReferenceTargetFromObject = (
|
|
408
|
+
object: AstNode,
|
|
409
|
+
namedEntityIds: ReadonlyMap<string, string>,
|
|
410
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
411
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
412
|
+
context?: ReadonlySet<string> | FrameworkNamespaceContext,
|
|
413
|
+
userNamespace?: ReadonlySet<string> | UserNamespaceContext
|
|
414
|
+
): string | null => {
|
|
415
|
+
if (object.type === 'Identifier') {
|
|
416
|
+
const bindingName = identifierName(object);
|
|
417
|
+
return bindingName
|
|
418
|
+
? deriveEntityIdentifierName(
|
|
419
|
+
bindingName,
|
|
420
|
+
namedEntityIds,
|
|
421
|
+
knownEntityIds,
|
|
422
|
+
importAliases
|
|
423
|
+
)
|
|
424
|
+
: null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const member = getEntityReferenceMember(object);
|
|
428
|
+
if (member) {
|
|
429
|
+
const namespaceTarget = getEntityReferenceTargetFromNamespaceMember(
|
|
430
|
+
member,
|
|
431
|
+
userNamespace
|
|
432
|
+
);
|
|
433
|
+
if (namespaceTarget) {
|
|
434
|
+
return namespaceTarget;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return extractEntityDefinition(object, context)?.name ?? null;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const ENTITY_ID_WRAPPER_METHODS = new Set([
|
|
442
|
+
'brand',
|
|
443
|
+
'catch',
|
|
444
|
+
'default',
|
|
445
|
+
'describe',
|
|
446
|
+
'meta',
|
|
447
|
+
'nullable',
|
|
448
|
+
'nullish',
|
|
449
|
+
'optional',
|
|
450
|
+
'readonly',
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
const getEntityIdCallMember = (
|
|
454
|
+
node: AstNode
|
|
455
|
+
): {
|
|
456
|
+
readonly member: NonNullable<ReturnType<typeof getEntityReferenceMember>>;
|
|
457
|
+
readonly propertyName: string;
|
|
458
|
+
} | null => {
|
|
459
|
+
const callee = node['callee'] as AstNode | undefined;
|
|
460
|
+
const member = callee ? getEntityReferenceMember(callee) : null;
|
|
461
|
+
const propertyName = member ? identifierName(member.property) : null;
|
|
462
|
+
return member && propertyName ? { member, propertyName } : null;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const getEntityIdCallObject = function getEntityIdCallObject(
|
|
466
|
+
node: AstNode | undefined
|
|
467
|
+
): AstNode | null {
|
|
468
|
+
const current = node;
|
|
469
|
+
if (!current || current.type !== 'CallExpression') {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const member = getEntityIdCallMember(current);
|
|
474
|
+
if (!member) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (member.propertyName === 'id') {
|
|
478
|
+
return member.member.object ?? null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return ENTITY_ID_WRAPPER_METHODS.has(member.propertyName)
|
|
482
|
+
? getEntityIdCallObject(member.member.object)
|
|
483
|
+
: null;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const extractEntityReferenceTarget = (
|
|
487
|
+
node: AstNode | undefined,
|
|
488
|
+
namedEntityIds: ReadonlyMap<string, string>,
|
|
489
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
490
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
491
|
+
context?: ReadonlySet<string> | FrameworkNamespaceContext,
|
|
492
|
+
userNamespace?: ReadonlySet<string> | UserNamespaceContext
|
|
493
|
+
): string | null => {
|
|
494
|
+
const object = getEntityIdCallObject(node);
|
|
495
|
+
return object
|
|
496
|
+
? getEntityReferenceTargetFromObject(
|
|
497
|
+
object,
|
|
498
|
+
namedEntityIds,
|
|
499
|
+
knownEntityIds,
|
|
500
|
+
importAliases,
|
|
501
|
+
context,
|
|
502
|
+
userNamespace
|
|
503
|
+
)
|
|
504
|
+
: null;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const getEntityShapeProperties = (
|
|
508
|
+
definition: EntityDefinition
|
|
509
|
+
): readonly AstNode[] =>
|
|
510
|
+
(definition.shape['properties'] as readonly AstNode[] | undefined) ?? [];
|
|
511
|
+
|
|
512
|
+
const buildEntityReferenceSite = (
|
|
513
|
+
definition: EntityDefinition,
|
|
514
|
+
property: AstNode,
|
|
515
|
+
namedEntityIds: ReadonlyMap<string, string>,
|
|
516
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
517
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
518
|
+
context?: ReadonlySet<string> | FrameworkNamespaceContext,
|
|
519
|
+
userNamespace?: ReadonlySet<string> | UserNamespaceContext
|
|
520
|
+
): EntityReferenceSite | null => {
|
|
521
|
+
if (property.type !== 'Property') {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const field = getPropertyName(property.key);
|
|
526
|
+
const target = extractEntityReferenceTarget(
|
|
527
|
+
property.value as AstNode | undefined,
|
|
528
|
+
namedEntityIds,
|
|
529
|
+
knownEntityIds,
|
|
530
|
+
importAliases,
|
|
531
|
+
context,
|
|
532
|
+
userNamespace
|
|
533
|
+
);
|
|
534
|
+
if (!field || !target) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
field,
|
|
540
|
+
source: definition.name,
|
|
541
|
+
start: property.start,
|
|
542
|
+
target,
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const findEntityReferenceSitesForDefinition = (
|
|
547
|
+
definition: EntityDefinition,
|
|
548
|
+
namedEntityIds: ReadonlyMap<string, string>,
|
|
549
|
+
knownEntityIds?: ReadonlySet<string>,
|
|
550
|
+
importAliases?: ReadonlyMap<string, string>,
|
|
551
|
+
context?: ReadonlySet<string> | FrameworkNamespaceContext,
|
|
552
|
+
userNamespace?: ReadonlySet<string> | UserNamespaceContext
|
|
553
|
+
): readonly EntityReferenceSite[] =>
|
|
554
|
+
getEntityShapeProperties(definition).flatMap((property) => {
|
|
555
|
+
const reference = buildEntityReferenceSite(
|
|
556
|
+
definition,
|
|
557
|
+
property,
|
|
558
|
+
namedEntityIds,
|
|
559
|
+
knownEntityIds,
|
|
560
|
+
importAliases,
|
|
561
|
+
context,
|
|
562
|
+
userNamespace
|
|
563
|
+
);
|
|
564
|
+
return reference ? [reference] : [];
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
/** Collect all entity field references declared via `.id()` in a parsed file. */
|
|
568
|
+
export const collectEntityReferenceSites = (
|
|
569
|
+
ast: AstNode,
|
|
570
|
+
knownEntityIds?: ReadonlySet<string>
|
|
571
|
+
): readonly EntityReferenceSite[] => {
|
|
572
|
+
const namedEntityIds = collectNamedEntityIds(ast);
|
|
573
|
+
const importAliases = collectImportAliasMap(ast);
|
|
574
|
+
const userNamespace = buildUserNamespaceContext(ast);
|
|
575
|
+
const context = buildFrameworkNamespaceContext(ast);
|
|
576
|
+
return findEntityDefinitions(ast, context).flatMap((definition) =>
|
|
577
|
+
findEntityReferenceSitesForDefinition(
|
|
578
|
+
definition,
|
|
579
|
+
namedEntityIds,
|
|
580
|
+
knownEntityIds,
|
|
581
|
+
importAliases,
|
|
582
|
+
context,
|
|
583
|
+
userNamespace
|
|
584
|
+
)
|
|
585
|
+
);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/** Collect entity reference targets keyed by source entity name. */
|
|
589
|
+
export const collectEntityReferenceTargetsByName = (
|
|
590
|
+
ast: AstNode,
|
|
591
|
+
knownEntityIds?: ReadonlySet<string>
|
|
592
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
593
|
+
const targetsByName = new Map<string, Set<string>>();
|
|
594
|
+
|
|
595
|
+
for (const reference of collectEntityReferenceSites(ast, knownEntityIds)) {
|
|
596
|
+
const existing = targetsByName.get(reference.source);
|
|
597
|
+
if (existing) {
|
|
598
|
+
existing.add(reference.target);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
targetsByName.set(reference.source, new Set([reference.target]));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return new Map(
|
|
606
|
+
[...targetsByName.entries()].map(([name, targets]) => [name, [...targets]])
|
|
607
|
+
);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// Implementation body extraction
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Extract top-level `implementation:` property values from an ObjectExpression's direct properties.
|
|
616
|
+
*
|
|
617
|
+
* Does not recurse into nested objects, so `meta: { implementation: ... }` is ignored.
|
|
618
|
+
*/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Warden-private pragma helpers. */
|
|
2
|
+
|
|
3
|
+
const WARDEN_IGNORE_NEXT_LINE_PRAGMAS = new Set([
|
|
4
|
+
'// warden-ignore-next-line',
|
|
5
|
+
'<!-- warden-ignore-next-line -->',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Split source code into lines for pragma lookups. Callers should split once
|
|
10
|
+
* per `check` invocation and thread the result through to
|
|
11
|
+
* {@link hasIgnoreCommentOnLine} so we avoid re-splitting the full source on
|
|
12
|
+
* every match in files with many draft-like string literals.
|
|
13
|
+
*/
|
|
14
|
+
export const splitSourceLines = (sourceCode: string): readonly string[] =>
|
|
15
|
+
sourceCode.split('\n');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check whether the line immediately preceding `line` contains a
|
|
19
|
+
* `warden-ignore-next-line` pragma (leading/trailing whitespace tolerated).
|
|
20
|
+
* Pragma scope is strictly one line — an intervening blank line breaks it.
|
|
21
|
+
*
|
|
22
|
+
* Takes a pre-split `lines` array so callers can split the source once per
|
|
23
|
+
* invocation instead of re-splitting for every literal they check.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* // warden-ignore-next-line
|
|
28
|
+
* const x = '_draft.intentional'; // suppressed
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const hasIgnoreCommentOnLine = (
|
|
32
|
+
lines: readonly string[],
|
|
33
|
+
line: number
|
|
34
|
+
): boolean => {
|
|
35
|
+
if (line <= 1) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const previous = lines[line - 2];
|
|
40
|
+
if (previous === undefined) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return WARDEN_IGNORE_NEXT_LINE_PRAGMAS.has(previous.trim());
|
|
45
|
+
};
|