@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42
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 +837 -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,310 @@
|
|
|
1
|
+
/** Warden-private store/factory pattern helpers. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractBindingName,
|
|
5
|
+
findConfigProperty,
|
|
6
|
+
getPropertyName,
|
|
7
|
+
identifierName,
|
|
8
|
+
walk,
|
|
9
|
+
} from '@ontrails/source';
|
|
10
|
+
import type { AstNode } from '@ontrails/source';
|
|
11
|
+
|
|
12
|
+
export interface StoreTableDefinition {
|
|
13
|
+
/** Table name declared inside store({ ... }). */
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/**
|
|
16
|
+
* Local binding name of the enclosing `store(...)` declaration, if the
|
|
17
|
+
* `store(...)` call is bound to a `const`/`let`/`var` (e.g. `db` in
|
|
18
|
+
* `const db = store({ ... })`). Null for anonymous stores.
|
|
19
|
+
*/
|
|
20
|
+
readonly storeBinding: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Stable composite key for this table in the form `${storeBinding}:${name}`,
|
|
23
|
+
* falling back to the bare `name` when the store is anonymous. Use this for
|
|
24
|
+
* compose-rule / compose-file keying so two stores with the same table name
|
|
25
|
+
* never collide.
|
|
26
|
+
*/
|
|
27
|
+
readonly key: string;
|
|
28
|
+
/** Start offset of the table property declaration. */
|
|
29
|
+
readonly start: number;
|
|
30
|
+
/** Whether the authored table opts into version tracking. */
|
|
31
|
+
readonly versioned: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a composite key for a store table: `${storeBinding}:${tableName}`,
|
|
36
|
+
* falling back to the bare `tableName` when the enclosing store has no local
|
|
37
|
+
* binding. Centralized so rule keying stays stable.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The key is intentionally file-local (no module path prefix). Compose-file
|
|
41
|
+
* aggregation in `ProjectContext` merges keys from all files, so two files
|
|
42
|
+
* with `const db = store({ notes: ... })` both produce `db:notes` — this is
|
|
43
|
+
* the desired behavior because the warden checks for *pattern completeness*
|
|
44
|
+
* across the project and matching keys signals that the same logical table
|
|
45
|
+
* is covered. If two genuinely different tables share a binding and name,
|
|
46
|
+
* that is a code-level naming collision the developer should resolve.
|
|
47
|
+
*/
|
|
48
|
+
export const makeStoreTableKey = (
|
|
49
|
+
storeBinding: string | null,
|
|
50
|
+
tableName: string
|
|
51
|
+
): string => (storeBinding ? `${storeBinding}:${tableName}` : tableName);
|
|
52
|
+
|
|
53
|
+
const isBooleanLiteral = (node: AstNode | undefined): boolean =>
|
|
54
|
+
Boolean(
|
|
55
|
+
node &&
|
|
56
|
+
((node.type === 'BooleanLiteral' &&
|
|
57
|
+
(node as unknown as { value?: unknown }).value === true) ||
|
|
58
|
+
(node.type === 'Literal' &&
|
|
59
|
+
(node as unknown as { value?: unknown }).value === true))
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a node is a `CallExpression` to the identifier `name`.
|
|
64
|
+
*
|
|
65
|
+
* e.g. `isNamedCall(node, 'store')` matches `store({...})` but not
|
|
66
|
+
* `someObj.store()` or `storeAlt()`.
|
|
67
|
+
*/
|
|
68
|
+
export const isNamedCall = (node: AstNode | undefined, name: string): boolean =>
|
|
69
|
+
!!node &&
|
|
70
|
+
node.type === 'CallExpression' &&
|
|
71
|
+
identifierName((node as unknown as { callee?: AstNode }).callee) === name;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Narrow a member-expression node (`a.b` or `a['b']`) to its `object` /
|
|
75
|
+
* `property` pair, returning `null` for anything else.
|
|
76
|
+
*/
|
|
77
|
+
export const getMemberExpression = (
|
|
78
|
+
node: AstNode | undefined
|
|
79
|
+
): { readonly object?: AstNode; readonly property?: AstNode } | null => {
|
|
80
|
+
if (
|
|
81
|
+
!node ||
|
|
82
|
+
(node.type !== 'MemberExpression' && node.type !== 'StaticMemberExpression')
|
|
83
|
+
) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return node as unknown as {
|
|
88
|
+
readonly object?: AstNode;
|
|
89
|
+
readonly property?: AstNode;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a `<store>.tables.<name>` member expression to its store binding
|
|
95
|
+
* and table name.
|
|
96
|
+
*
|
|
97
|
+
* Returns `null` for anything that isn't a two-level member access ending in
|
|
98
|
+
* `.tables.<name>`. The store binding is the identifier of the object owning
|
|
99
|
+
* `.tables` — typically the local binding from `const db = store(...)`.
|
|
100
|
+
*/
|
|
101
|
+
export const extractStoreTableFromMember = (
|
|
102
|
+
node: AstNode | undefined
|
|
103
|
+
): {
|
|
104
|
+
readonly storeBinding: string | null;
|
|
105
|
+
readonly tableName: string;
|
|
106
|
+
} | null => {
|
|
107
|
+
const member = getMemberExpression(node);
|
|
108
|
+
const tableName = member ? getPropertyName(member.property) : null;
|
|
109
|
+
const tablesMember = member ? getMemberExpression(member.object) : null;
|
|
110
|
+
if (!tableName || !tablesMember) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (getPropertyName(tablesMember.property) !== 'tables') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const storeBinding = identifierName(tablesMember.object) ?? null;
|
|
119
|
+
return { storeBinding, tableName };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Collect `const foo = <store>.tables.<name>` bindings from a parsed file,
|
|
124
|
+
* keyed by the local binding name. Values are the composite table key
|
|
125
|
+
* (`${storeBinding}:${tableName}`) so callers can dedupe across stores that
|
|
126
|
+
* share a table name.
|
|
127
|
+
*/
|
|
128
|
+
export const collectNamedStoreTableIds = (
|
|
129
|
+
ast: AstNode
|
|
130
|
+
): ReadonlyMap<string, string> => {
|
|
131
|
+
const ids = new Map<string, string>();
|
|
132
|
+
|
|
133
|
+
walk(ast, (node) => {
|
|
134
|
+
if (node.type !== 'VariableDeclarator') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { id, init } = node as unknown as {
|
|
139
|
+
readonly id?: AstNode;
|
|
140
|
+
readonly init?: AstNode;
|
|
141
|
+
};
|
|
142
|
+
const name = extractBindingName(id);
|
|
143
|
+
const table = extractStoreTableFromMember(init);
|
|
144
|
+
if (name && table) {
|
|
145
|
+
ids.set(name, makeStoreTableKey(table.storeBinding, table.tableName));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return ids;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve an argument node to a composite store-table key
|
|
154
|
+
* (`${storeBinding}:${tableName}` or bare `tableName` when anonymous).
|
|
155
|
+
*
|
|
156
|
+
* Handles the two authoring patterns:
|
|
157
|
+
* - direct member access: `db.tables.notes`
|
|
158
|
+
* - identifier reference: `const notesTable = db.tables.notes; crud(notesTable, …)`
|
|
159
|
+
*/
|
|
160
|
+
export const deriveStoreTableId = (
|
|
161
|
+
node: AstNode | undefined,
|
|
162
|
+
namedStoreTableIds: ReadonlyMap<string, string>
|
|
163
|
+
): string | null => {
|
|
164
|
+
if (!node) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (node.type === 'Identifier') {
|
|
169
|
+
const name = identifierName(node);
|
|
170
|
+
return name ? (namedStoreTableIds.get(name) ?? null) : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const member = extractStoreTableFromMember(node);
|
|
174
|
+
return member
|
|
175
|
+
? makeStoreTableKey(member.storeBinding, member.tableName)
|
|
176
|
+
: null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const extractStoreTableDefinitions = (
|
|
180
|
+
node: AstNode,
|
|
181
|
+
storeBinding: string | null
|
|
182
|
+
): readonly StoreTableDefinition[] => {
|
|
183
|
+
if (!isNamedCall(node, 'store')) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const [tablesArg] = ((node as unknown as { arguments?: readonly AstNode[] })
|
|
188
|
+
.arguments ?? []) as readonly AstNode[];
|
|
189
|
+
if (!tablesArg || tablesArg.type !== 'ObjectExpression') {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const properties = tablesArg['properties'] as readonly AstNode[] | undefined;
|
|
194
|
+
if (!properties) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return properties.flatMap((property) => {
|
|
199
|
+
if (property.type !== 'Property') {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const name = getPropertyName(property.key);
|
|
204
|
+
const value = property.value as AstNode | undefined;
|
|
205
|
+
if (!name || value?.type !== 'ObjectExpression') {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const versionedProp = findConfigProperty(value, 'versioned');
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
key: makeStoreTableKey(storeBinding, name),
|
|
213
|
+
name,
|
|
214
|
+
start: property.start,
|
|
215
|
+
storeBinding,
|
|
216
|
+
versioned: isBooleanLiteral(
|
|
217
|
+
versionedProp?.value as AstNode | undefined
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const findStoreTableDefinitions = (
|
|
225
|
+
ast: AstNode
|
|
226
|
+
): readonly StoreTableDefinition[] => {
|
|
227
|
+
const definitions: StoreTableDefinition[] = [];
|
|
228
|
+
const seenStoreCalls = new WeakSet<AstNode>();
|
|
229
|
+
|
|
230
|
+
// First pass: bound stores (walk VariableDeclarators so we know the binding).
|
|
231
|
+
walk(ast, (node) => {
|
|
232
|
+
if (node.type !== 'VariableDeclarator') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { id, init } = node as unknown as {
|
|
237
|
+
readonly id?: AstNode;
|
|
238
|
+
readonly init?: AstNode;
|
|
239
|
+
};
|
|
240
|
+
if (!init || !isNamedCall(init, 'store')) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
seenStoreCalls.add(init);
|
|
245
|
+
const storeBinding = extractBindingName(id);
|
|
246
|
+
definitions.push(...extractStoreTableDefinitions(init, storeBinding));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Second pass: anonymous `store({...})` calls not bound to a variable
|
|
250
|
+
// (e.g. an inline default export). Use the bare table name as the key.
|
|
251
|
+
walk(ast, (node) => {
|
|
252
|
+
if (!isNamedCall(node, 'store') || seenStoreCalls.has(node)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
definitions.push(...extractStoreTableDefinitions(node, null));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return definitions;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const collectCrudTableIds = (ast: AstNode): ReadonlySet<string> => {
|
|
262
|
+
const ids = new Set<string>();
|
|
263
|
+
const namedStoreTableIds = collectNamedStoreTableIds(ast);
|
|
264
|
+
|
|
265
|
+
walk(ast, (node) => {
|
|
266
|
+
if (!isNamedCall(node, 'crud')) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const [tableArg] = ((node as unknown as { arguments?: readonly AstNode[] })
|
|
271
|
+
.arguments ?? []) as readonly AstNode[];
|
|
272
|
+
const tableId = deriveStoreTableId(tableArg, namedStoreTableIds);
|
|
273
|
+
if (tableId) {
|
|
274
|
+
ids.add(tableId);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return ids;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const collectReconcileTableIds = (ast: AstNode): ReadonlySet<string> => {
|
|
282
|
+
const ids = new Set<string>();
|
|
283
|
+
const namedStoreTableIds = collectNamedStoreTableIds(ast);
|
|
284
|
+
|
|
285
|
+
walk(ast, (node) => {
|
|
286
|
+
if (!isNamedCall(node, 'reconcile')) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const [configArg] = ((
|
|
291
|
+
node as unknown as {
|
|
292
|
+
arguments?: readonly AstNode[];
|
|
293
|
+
}
|
|
294
|
+
).arguments ?? []) as readonly AstNode[];
|
|
295
|
+
if (!configArg || configArg.type !== 'ObjectExpression') {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const tableProp = findConfigProperty(configArg, 'table');
|
|
300
|
+
const tableId = deriveStoreTableId(
|
|
301
|
+
tableProp?.value as AstNode | undefined,
|
|
302
|
+
namedStoreTableIds
|
|
303
|
+
);
|
|
304
|
+
if (tableId) {
|
|
305
|
+
ids.add(tableId);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return ids;
|
|
310
|
+
};
|
package/src/rules/specs.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface ObjectProperty extends ParsedEntry {
|
|
|
18
18
|
|
|
19
19
|
export interface TrailLikeSpec {
|
|
20
20
|
readonly id: string;
|
|
21
|
-
readonly kind: '
|
|
21
|
+
readonly kind: 'signal' | 'trail';
|
|
22
22
|
readonly line: number;
|
|
23
23
|
readonly properties: ReadonlyMap<string, ObjectProperty>;
|
|
24
24
|
readonly specText: string;
|
|
@@ -31,7 +31,11 @@ export interface SchemaFieldInfo {
|
|
|
31
31
|
readonly required: boolean;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Match `trail(...)` / `signal(...)` declaration sites, not method calls.
|
|
35
|
+
// The negative lookbehind excludes `foo.signal(...)`, `foo?.signal(...)`, and
|
|
36
|
+
// similar member-expression call sites (including optional chaining with
|
|
37
|
+
// whitespace) where `signal` / `trail` is a property name, not a factory.
|
|
38
|
+
const TRAIL_LIKE_PATTERN = /(?<![.?])\b(trail|signal)\s*\(/g;
|
|
35
39
|
|
|
36
40
|
const PROPERTY_PATTERN =
|
|
37
41
|
/^(?:readonly\s+)?(?:(["'`])([^"'`]+)\1|([A-Za-z_$][\w$]*))\s*:\s*([\s\S]+)$/;
|
|
@@ -214,7 +218,7 @@ const resolveSpecId = (
|
|
|
214
218
|
|
|
215
219
|
const buildTrailLikeSpec = (
|
|
216
220
|
sourceCode: string,
|
|
217
|
-
kind: '
|
|
221
|
+
kind: 'signal' | 'trail',
|
|
218
222
|
specArg: SplitEntry,
|
|
219
223
|
specStart: number,
|
|
220
224
|
id: string,
|
|
@@ -276,7 +280,7 @@ const resolveTrailLikeSpec = (
|
|
|
276
280
|
|
|
277
281
|
const parseTrailLikeMatch = (
|
|
278
282
|
sourceCode: string,
|
|
279
|
-
kind: '
|
|
283
|
+
kind: 'signal' | 'trail',
|
|
280
284
|
callStart: number
|
|
281
285
|
): TrailLikeSpec | null => {
|
|
282
286
|
const resolved = resolveTrailLikeSpec(sourceCode, callStart);
|
|
@@ -352,7 +356,7 @@ export const findTrailLikeSpecs = (
|
|
|
352
356
|
continue;
|
|
353
357
|
}
|
|
354
358
|
|
|
355
|
-
const kind = match[1] === '
|
|
359
|
+
const kind = match[1] === 'signal' ? 'signal' : 'trail';
|
|
356
360
|
const spec = parseTrailLikeMatch(sourceCode, kind, callStart);
|
|
357
361
|
if (spec !== null) {
|
|
358
362
|
specs.push(spec);
|