@starci/hfs 4.0.10 → 4.1.0
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 +1 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +3 -2
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +221 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +98 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +92 -256
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { checkerScope } from './required-files.mjs';
|
|
4
4
|
import { allowsFile } from '../allows.mjs';
|
|
5
5
|
import { unwrapEach } from './ast-walks.mjs';
|
|
6
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* R47 `contract-fixture-guard` (BE_CONTRACT_UNGUARDED). A fake at the network edge serves payload fixtures; a fixture drifts
|
|
@@ -37,51 +38,87 @@ function walk(ts, root, visit) {
|
|
|
37
38
|
step(root);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const importsFakesOf = (from, provider) => from.includes(`${FAKES_DIRECTORY}${provider}/`) || from.endsWith(`${FAKES_DIRECTORY}${provider}`);
|
|
42
|
+
|
|
43
|
+
const shapeNamesOf = (ts, statement, helper) => {
|
|
44
|
+
const named = statement.importClause?.namedBindings;
|
|
45
|
+
if (!named || !ts.isNamedImports(named)) return [];
|
|
46
|
+
return named.elements.filter(element => (element.propertyName ?? element.name).text === helper).map(element => element.name.text);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const importsOfSpec = (ts, sourceFile, helper, providers) => {
|
|
42
50
|
const shapeNames = new Set();
|
|
43
51
|
const referenced = new Set();
|
|
44
52
|
for (const statement of sourceFile.statements) {
|
|
45
53
|
if (!ts.isImportDeclaration(statement)) continue;
|
|
46
54
|
const from = statement.moduleSpecifier.text;
|
|
47
|
-
for (const provider of providers) if (from
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
for (const provider of providers) if (importsFakesOf(from, provider)) referenced.add(provider);
|
|
56
|
+
for (const name of shapeNamesOf(ts, statement, helper)) shapeNames.add(name);
|
|
57
|
+
}
|
|
58
|
+
return { shapeNames, referenced };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const callsShape = (ts, node, shapeNames) => {
|
|
62
|
+
let found = false;
|
|
63
|
+
walk(ts, node, inner => { if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) && shapeNames.has(inner.expression.text)) found = true; });
|
|
64
|
+
return found;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const TEST_BLOCKS = new Set(['describe', 'it', 'test']);
|
|
68
|
+
|
|
69
|
+
const referenceProviders = (ts, node, providers, referenced) => {
|
|
70
|
+
if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
|
|
71
|
+
if (!ts.isCallExpression(node)) return;
|
|
72
|
+
const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
|
|
73
|
+
for (const argument of node.arguments) {
|
|
74
|
+
const value = unwrap(ts, argument);
|
|
75
|
+
if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && !TEST_BLOCKS.has(named)) referenced.add(value.text);
|
|
51
76
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** The `expect(...)` call a shape matcher call hangs off (through `.not` / `.resolves`), or null. */
|
|
80
|
+
const expectReceiverOf = (ts, callee) => {
|
|
81
|
+
let receiver = unwrap(ts, callee.expression);
|
|
82
|
+
while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression);
|
|
83
|
+
return receiver && ts.isCallExpression(receiver) && ts.isIdentifier(receiver.expression) && receiver.expression.text === 'expect' ? receiver : null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const comparesShapes = (ts, node, shapeNames) => {
|
|
87
|
+
if (!ts.isCallExpression(node)) return false;
|
|
88
|
+
const callee = node.expression;
|
|
89
|
+
if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return false;
|
|
90
|
+
const receiver = expectReceiverOf(ts, callee);
|
|
91
|
+
if (!receiver) return false;
|
|
92
|
+
const [actual] = receiver.arguments;
|
|
93
|
+
const [expected] = node.arguments;
|
|
94
|
+
return Boolean(actual && expected && callsShape(ts, actual, shapeNames) && callsShape(ts, expected, shapeNames));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const referenceAndCompare = (ts, node, providers, referenced, shapeNames) => {
|
|
98
|
+
referenceProviders(ts, node, providers, referenced);
|
|
99
|
+
return comparesShapes(ts, node, shapeNames);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Facts one contract spec establishes: the providers whose fixtures it references and whether it compares shapes. */
|
|
103
|
+
function readSpec(ts, sourceFile, helper, providers) {
|
|
104
|
+
const { shapeNames, referenced } = importsOfSpec(ts, sourceFile, helper, providers);
|
|
57
105
|
let compares = false;
|
|
58
|
-
walk(ts, sourceFile, node => {
|
|
59
|
-
if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
|
|
60
|
-
if (!ts.isCallExpression(node)) return;
|
|
61
|
-
const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
|
|
62
|
-
for (const argument of node.arguments) {
|
|
63
|
-
const value = unwrap(ts, argument);
|
|
64
|
-
if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && named !== 'describe' && named !== 'it' && named !== 'test') referenced.add(value.text);
|
|
65
|
-
}
|
|
66
|
-
const callee = node.expression;
|
|
67
|
-
if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return;
|
|
68
|
-
let receiver = unwrap(ts, callee.expression);
|
|
69
|
-
while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression); // .not / .resolves
|
|
70
|
-
if (!receiver || !ts.isCallExpression(receiver) || !ts.isIdentifier(receiver.expression) || receiver.expression.text !== 'expect') return;
|
|
71
|
-
const [actual] = receiver.arguments;
|
|
72
|
-
const [expected] = node.arguments;
|
|
73
|
-
if (actual && expected && callsShape(actual) && callsShape(expected)) compares = true;
|
|
74
|
-
});
|
|
106
|
+
walk(ts, sourceFile, node => { if (referenceAndCompare(ts, node, providers, referenced, shapeNames)) compares = true; });
|
|
75
107
|
return { referenced, compares };
|
|
76
108
|
}
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
110
|
+
/** The payload fixture a file of the world slot is, as `[provider, file]`, or null. */
|
|
111
|
+
const payloadFixtureOf = (resolver, file) => {
|
|
112
|
+
const verdict = allowsFile(resolver, file);
|
|
113
|
+
if (verdict?.slot !== WORLD_SLOT || !verdict.relative.startsWith(FAKES_DIRECTORY)) return null;
|
|
114
|
+
const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
|
|
115
|
+
return match ? [match[1], file] : null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const fixtureFilesOf = (tree, resolver) => {
|
|
82
119
|
const fixtures = new Map(); // provider -> first payload fixture file
|
|
83
120
|
const specs = new Map(); // provider -> [contract spec file]
|
|
84
|
-
for (const file of [...tree.files].sort()) {
|
|
121
|
+
for (const file of [...tree.files].sort(byCodeUnit)) {
|
|
85
122
|
const classified = resolver.classifyPath(file);
|
|
86
123
|
if (classified.status !== 'owned') continue;
|
|
87
124
|
if (classified.slot === CONTRACT_SLOT) {
|
|
@@ -89,14 +126,13 @@ export function checkContractFixtureGuard(input) {
|
|
|
89
126
|
if (provider && file.endsWith('.contract-spec.ts')) specs.set(provider, [...(specs.get(provider) ?? []), file]);
|
|
90
127
|
continue;
|
|
91
128
|
}
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
if (!verdict.relative.startsWith(FAKES_DIRECTORY)) continue;
|
|
95
|
-
const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
|
|
96
|
-
if (match && !fixtures.has(match[1])) fixtures.set(match[1], file);
|
|
97
|
-
}
|
|
129
|
+
const fixture = classified.slot === WORLD_SLOT ? payloadFixtureOf(resolver, file) : null;
|
|
130
|
+
if (fixture && !fixtures.has(fixture[0])) fixtures.set(...fixture);
|
|
98
131
|
}
|
|
99
|
-
|
|
132
|
+
return { fixtures, specs };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const guardedProvidersOf = (config, ts, contractShape, providers, specs) => {
|
|
100
136
|
const guarded = new Set();
|
|
101
137
|
for (const [provider, files] of specs) {
|
|
102
138
|
for (const file of files) {
|
|
@@ -107,6 +143,16 @@ export function checkContractFixtureGuard(input) {
|
|
|
107
143
|
if (facts.compares && facts.referenced.has(provider)) guarded.add(provider);
|
|
108
144
|
}
|
|
109
145
|
}
|
|
146
|
+
return guarded;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export function checkContractFixtureGuard(input) {
|
|
150
|
+
const { config, ts, resolver, tree } = checkerScope(input);
|
|
151
|
+
const { contractShape } = resolver.ruleParams();
|
|
152
|
+
const violations = [];
|
|
153
|
+
const { fixtures, specs } = fixtureFilesOf(tree, resolver);
|
|
154
|
+
const providers = new Set(fixtures.keys());
|
|
155
|
+
const guarded = guardedProvidersOf(config, ts, contractShape, providers, specs);
|
|
110
156
|
for (const [provider, file] of [...fixtures].sort(([a], [b]) => a.localeCompare(b))) {
|
|
111
157
|
if (guarded.has(provider)) continue;
|
|
112
158
|
const has = specs.get(provider)?.length ?? 0;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
3
|
+
import { anyDescendant, nodeDecorators, normalizedSymbol, normalizedSymbolValue, valueSymbol, violation } from './ast-walks.mjs';
|
|
4
|
+
import { hasAnyFlag, isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
|
+
|
|
6
|
+
export const READONLY_BOUNDARY_RULE_ID = 'BE_READONLY_BOUNDARY';
|
|
7
|
+
|
|
8
|
+
export function canonical(file) {
|
|
9
|
+
return path.resolve(file);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function modifiers(ts, node) {
|
|
13
|
+
return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasModifier(ts, node, kind) {
|
|
17
|
+
return modifiers(ts, node).some(modifier => modifier.kind === kind);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isPublicMember(ts, node) {
|
|
21
|
+
return !hasModifier(ts, node, ts.SyntaxKind.PrivateKeyword) && !hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readonlyMember(ts, node) {
|
|
25
|
+
return hasModifier(ts, node, ts.SyntaxKind.ReadonlyKeyword);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function propertyName(ts, node) {
|
|
29
|
+
const name = node.name;
|
|
30
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isPrivateIdentifier(name)) return name.text;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function thisOriginStatus(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
35
|
+
if (!expression || depth > 8) return 'no';
|
|
36
|
+
expression = unwrapExpression(ts, expression);
|
|
37
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword) return 'yes';
|
|
38
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
39
|
+
if (!symbol || seen.has(symbol)) return 'no';
|
|
40
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
41
|
+
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer) return 'no';
|
|
42
|
+
const origin = thisOriginStatus(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
|
|
43
|
+
if (origin === 'no') return 'no';
|
|
44
|
+
return isConstVariable(ts, declarations[0]) ? origin : 'unavailable';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function instanceAssignmentTarget(ts, checker, expression) {
|
|
48
|
+
expression = unwrapExpression(ts, expression);
|
|
49
|
+
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return { instance: false };
|
|
50
|
+
const owner = thisOriginStatus(ts, checker, expression.expression);
|
|
51
|
+
if (owner === 'no') return { instance: false };
|
|
52
|
+
if (owner === 'unavailable') return { instance: true, name: null, supported: false };
|
|
53
|
+
if (ts.isPropertyAccessExpression(expression)) return { instance: true, name: expression.name.text, supported: true };
|
|
54
|
+
if (ts.isStringLiteralLike(expression.argumentExpression)) return { instance: true, name: expression.argumentExpression.text, supported: true };
|
|
55
|
+
return { instance: true, name: null, supported: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function expressionOrigin(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
59
|
+
if (!expression || depth > 8) return null;
|
|
60
|
+
expression = unwrapExpression(ts, expression);
|
|
61
|
+
const shorthand = ts.isShorthandPropertyAssignment(expression) ? checker.getShorthandAssignmentValueSymbol?.(expression) : null;
|
|
62
|
+
const symbol = normalizedSymbolValue(ts, checker, shorthand) ?? normalizedSymbol(ts, checker, expression);
|
|
63
|
+
if (!symbol || seen.has(symbol)) return symbol;
|
|
64
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
65
|
+
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
66
|
+
&& isConstVariable(ts, declarations[0])) {
|
|
67
|
+
return expressionOrigin(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
|
|
68
|
+
}
|
|
69
|
+
return symbol;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const expressionReferencesOrigin = (ts, checker, expression, expected) =>
|
|
73
|
+
anyDescendant(ts, expression, node => expressionOrigin(ts, checker, node) === expected);
|
|
74
|
+
|
|
75
|
+
function isSafePrimitiveProjection(ts, checker, expression, expected) {
|
|
76
|
+
expression = unwrapExpression(ts, expression);
|
|
77
|
+
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false;
|
|
78
|
+
if (expressionOrigin(ts, checker, expression.expression) !== expected) return false;
|
|
79
|
+
const type = checker.getTypeAtLocation(expression);
|
|
80
|
+
const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike
|
|
81
|
+
| ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
82
|
+
| ts.TypeFlags.Void | ts.TypeFlags.Never;
|
|
83
|
+
return hasAnyFlag(type.flags, primitive) && !hasAnyFlag(type.flags, ts.TypeFlags.Any | ts.TypeFlags.Unknown);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assignmentPatternHasInstanceTarget(ts, checker, expression) {
|
|
87
|
+
let found = false;
|
|
88
|
+
const visit = node => {
|
|
89
|
+
if (found) return;
|
|
90
|
+
const target = instanceAssignmentTarget(ts, checker, node);
|
|
91
|
+
if (target.instance) {
|
|
92
|
+
found = true;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
ts.forEachChild(node, visit);
|
|
96
|
+
};
|
|
97
|
+
visit(expression);
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function globalLibraryIdentifier(ts, checker, node, expected) {
|
|
102
|
+
if (!ts.isIdentifier(node) || node.text !== expected) return false;
|
|
103
|
+
const symbol = normalizedSymbol(ts, checker, node);
|
|
104
|
+
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
105
|
+
return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile
|
|
106
|
+
&& /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function instanceMutationCall(ts, checker, node) {
|
|
110
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
|
|
111
|
+
const owner = node.expression.expression;
|
|
112
|
+
const method = node.expression.name.text;
|
|
113
|
+
const known = (method === 'assign' || method === 'defineProperty') && globalLibraryIdentifier(ts, checker, owner, 'Object')
|
|
114
|
+
|| method === 'set' && globalLibraryIdentifier(ts, checker, owner, 'Reflect');
|
|
115
|
+
if (!known || thisOriginStatus(ts, checker, node.arguments[0]) === 'no') return null;
|
|
116
|
+
return { node, values: node.arguments.slice(method === 'assign' ? 1 : 2) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isFunctionBoundary = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
120
|
+
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
|
|
121
|
+
|
|
122
|
+
/** The instance assignment a `<left> = <origin-derived>` expression performs, or null when its target is no instance member. */
|
|
123
|
+
function instanceAssignment(ts, checker, node) {
|
|
124
|
+
const target = instanceAssignmentTarget(ts, checker, node.left);
|
|
125
|
+
if (target.instance) {
|
|
126
|
+
return { node, name: target.name, supported: target.supported && node.operatorToken.kind === ts.SyntaxKind.EqualsToken };
|
|
127
|
+
}
|
|
128
|
+
return assignmentPatternHasInstanceTarget(ts, checker, node.left) ? { node, name: null, supported: false } : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The assignment or mutation call at `node` that stores the constructor parameter on the instance, or null. */
|
|
132
|
+
function constructorFinding(ts, checker, node, parameterSymbol) {
|
|
133
|
+
if (ts.isBinaryExpression(node) && expressionReferencesOrigin(ts, checker, node.right, parameterSymbol)
|
|
134
|
+
&& !isSafePrimitiveProjection(ts, checker, node.right, parameterSymbol)) {
|
|
135
|
+
return instanceAssignment(ts, checker, node);
|
|
136
|
+
}
|
|
137
|
+
const mutation = instanceMutationCall(ts, checker, node);
|
|
138
|
+
return mutation?.values.some(argument => expressionReferencesOrigin(ts, checker, argument, parameterSymbol))
|
|
139
|
+
? { node, name: null, supported: false }
|
|
140
|
+
: null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function constructorAssignments(ts, checker, constructor, parameter) {
|
|
144
|
+
if (!constructor.body) return [];
|
|
145
|
+
const parameterSymbol = normalizedSymbol(ts, checker, parameter.name);
|
|
146
|
+
const assignments = [];
|
|
147
|
+
const visit = node => {
|
|
148
|
+
if (node !== constructor.body && isFunctionBoundary(ts, node)) return;
|
|
149
|
+
const finding = constructorFinding(ts, checker, node, parameterSymbol);
|
|
150
|
+
if (finding) assignments.push(finding);
|
|
151
|
+
else ts.forEachChild(node, visit);
|
|
152
|
+
};
|
|
153
|
+
visit(constructor.body);
|
|
154
|
+
return assignments;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function checkReadonlyProperty(config, context, sourceFile, node, label, violations) {
|
|
158
|
+
if (!readonlyMember(context.ts, node)) violations.push(violation(config, sourceFile, node.name ?? node, READONLY_BOUNDARY_RULE_ID,
|
|
159
|
+
`${label} must be readonly; execution state must not mutate an injected dependency or received message.`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function classValuesForSymbol(ts, symbol, localFiles) {
|
|
163
|
+
return (symbol?.getDeclarations?.() ?? []).flatMap(declaration => {
|
|
164
|
+
if (ts.isClassDeclaration(declaration)) return [declaration];
|
|
165
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
166
|
+
const initializer = unwrapExpression(ts, declaration.initializer);
|
|
167
|
+
if (ts.isClassExpression(initializer)) return [initializer];
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function messageClass(config, context, checker, decorator, localFiles, reasons) {
|
|
174
|
+
const expression = decorator.expression;
|
|
175
|
+
if (!context.ts.isCallExpression(expression) || expression.arguments.length < 1) {
|
|
176
|
+
reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} has a handler decorator without a static message class`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const symbol = valueSymbol(context.ts, checker, expression.arguments[0]);
|
|
180
|
+
const declarations = classValuesForSymbol(context.ts, symbol, localFiles);
|
|
181
|
+
if (declarations.length !== 1) {
|
|
182
|
+
reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} cannot resolve one local message class from its handler decorator`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return declarations[0];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function inspectMessageConstructor(state, constructor) {
|
|
189
|
+
const { config, context, checker, sourceFile, violations, reasons } = state;
|
|
190
|
+
const ts = context.ts;
|
|
191
|
+
for (const parameter of constructor.parameters) {
|
|
192
|
+
if (modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword,
|
|
193
|
+
ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind))) {
|
|
194
|
+
checkReadonlyProperty(config, context, sourceFile, parameter, `Message field ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!constructor.body) return;
|
|
198
|
+
const inspectAssign = node => {
|
|
199
|
+
if (node !== constructor.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
|
|
200
|
+
if (instanceMutationCall(ts, checker, node)) {
|
|
201
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance mutation`);
|
|
202
|
+
} else if (ts.isBinaryExpression(node) && assignmentPatternHasInstanceTarget(ts, checker, node.left)
|
|
203
|
+
&& !instanceAssignmentTarget(ts, checker, node.left).supported) {
|
|
204
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance assignment`);
|
|
205
|
+
} else ts.forEachChild(node, inspectAssign);
|
|
206
|
+
};
|
|
207
|
+
inspectAssign(constructor.body);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function inspectMessageMembers(state) {
|
|
211
|
+
const { config, context, declaration, violations, reasons } = state;
|
|
212
|
+
const ts = context.ts;
|
|
213
|
+
const sourceFile = declaration.getSourceFile();
|
|
214
|
+
const classState = { ...state, sourceFile };
|
|
215
|
+
for (const member of declaration.members) {
|
|
216
|
+
if (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword)) {
|
|
217
|
+
checkReadonlyProperty(config, context, sourceFile, member, `Message field ${propertyName(ts, member) ?? '(computed)'}`, violations);
|
|
218
|
+
}
|
|
219
|
+
if (ts.isSetAccessorDeclaration(member)) violations.push(violation(config, sourceFile, member.name, READONLY_BOUNDARY_RULE_ID,
|
|
220
|
+
`Message field ${propertyName(ts, member) ?? '(computed)'} cannot expose a setter.`));
|
|
221
|
+
if (ts.isIndexSignatureDeclaration(member)) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has a message index signature whose mutation boundary cannot be proved`);
|
|
222
|
+
if (ts.isConstructorDeclaration(member)) inspectMessageConstructor(classState, member);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function classCarriesState(ts, declaration) {
|
|
227
|
+
return declaration.members.some(member => (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword))
|
|
228
|
+
|| ts.isSetAccessorDeclaration(member) || ts.isIndexSignatureDeclaration(member)
|
|
229
|
+
|| (ts.isConstructorDeclaration(member) && member.parameters.length > 0));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function classInstanceType(ts, checker, declaration) {
|
|
233
|
+
const symbol = declaration.name ? normalizedSymbol(ts, checker, declaration.name) : null;
|
|
234
|
+
if (symbol && ts.isClassDeclaration(declaration)) return checker.getDeclaredTypeOfSymbol(symbol);
|
|
235
|
+
const value = checker.getTypeAtLocation(declaration);
|
|
236
|
+
const constructor = checker.getSignaturesOfType(value, ts.SignatureKind.Construct)[0];
|
|
237
|
+
return constructor ? checker.getReturnTypeOfSignature(constructor) : value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function inspectMessageBases(state) {
|
|
241
|
+
const { config, context, checker, declaration, localFiles, sourceFile, reasons } = state;
|
|
242
|
+
const ts = context.ts;
|
|
243
|
+
const type = classInstanceType(ts, checker, declaration);
|
|
244
|
+
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
245
|
+
const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
246
|
+
if (declarations.length !== 1) {
|
|
247
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
|
|
248
|
+
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
|
|
249
|
+
visitMessageClass({ ...state, declaration: declarations[0], sourceFile: declarations[0].getSourceFile() });
|
|
250
|
+
} else if (classCarriesState(ts, declarations[0])) {
|
|
251
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message state from a class outside the checked production program`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function visitMessageClass(state) {
|
|
257
|
+
const { declaration, visited } = state;
|
|
258
|
+
const key = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
|
|
259
|
+
if (visited.has(key)) return;
|
|
260
|
+
visited.add(key);
|
|
261
|
+
const classState = { ...state, sourceFile: declaration.getSourceFile() };
|
|
262
|
+
inspectMessageMembers(classState);
|
|
263
|
+
inspectMessageBases(classState);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function checkMessageReadonly(config, context, checker, declaration, localFiles, violations, reasons) {
|
|
267
|
+
visitMessageClass({ config, context, checker, declaration, localFiles, visited: new Set(), violations, reasons });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function inspectInjectedParameter(state, member, parameter) {
|
|
271
|
+
const { config, context, checker, sourceFile, classKind, properties, framework, violations, reasons } = state;
|
|
272
|
+
const ts = context.ts;
|
|
273
|
+
const explicitInject = nodeDecorators(ts, parameter).some(decorator => framework.decoratorKind(ts, checker, decorator, framework.targets) === 'Inject');
|
|
274
|
+
if (!classKind && !explicitInject) return;
|
|
275
|
+
const parameterProperty = modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
|
|
276
|
+
ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind));
|
|
277
|
+
if (parameterProperty) {
|
|
278
|
+
checkReadonlyProperty(config, context, sourceFile, parameter, `Injected dependency ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
for (const assignment of constructorAssignments(ts, checker, member, parameter)) {
|
|
282
|
+
if (!assignment.supported || !assignment.name) {
|
|
283
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)}:${sourceLocation(sourceFile, assignment.node).line} stores an injected dependency through an unsupported instance assignment`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const property = properties.get(assignment.name);
|
|
287
|
+
if (!property) reasons.push(`${relativePath(config.root, sourceFile.fileName)} assigns injected dependency ${assignment.name} without a declared field`);
|
|
288
|
+
else checkReadonlyProperty(config, context, sourceFile, property, `Injected dependency ${assignment.name}`, violations);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function inspectInjectedConstructor(state, member) {
|
|
293
|
+
for (const parameter of member.parameters) {
|
|
294
|
+
inspectInjectedParameter(state, member, parameter);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function inspectInjectedMembers(state) {
|
|
299
|
+
const { config, context, checker, declaration, framework, violations } = state;
|
|
300
|
+
const ts = context.ts;
|
|
301
|
+
const sourceFile = declaration.getSourceFile();
|
|
302
|
+
const classState = { ...state, sourceFile };
|
|
303
|
+
for (const member of declaration.members) {
|
|
304
|
+
if (ts.isPropertyDeclaration(member)) {
|
|
305
|
+
const injected = nodeDecorators(ts, member).some(decorator => framework.decoratorKind(ts, checker, decorator, framework.targets) === 'Inject');
|
|
306
|
+
if (injected) checkReadonlyProperty(config, context, sourceFile, member, `Injected property ${propertyName(ts, member) ?? '(computed)'}`, violations);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!ts.isConstructorDeclaration(member)) continue;
|
|
310
|
+
inspectInjectedConstructor(classState, member);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function inspectInjectedBases(state) {
|
|
315
|
+
const { config, context, checker, declaration, localFiles, reasons } = state;
|
|
316
|
+
const ts = context.ts;
|
|
317
|
+
const sourceFile = declaration.getSourceFile();
|
|
318
|
+
const type = classInstanceType(ts, checker, declaration);
|
|
319
|
+
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
320
|
+
const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
321
|
+
if (declarations.length !== 1) {
|
|
322
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
323
|
+
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
|
|
324
|
+
checkInjectedClass({ ...state, declaration: declarations[0] });
|
|
325
|
+
} else if (classCarriesState(ts, declarations[0])) {
|
|
326
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function checkInjectedClass({ config, context, checker, declaration, classKind, targets, decoratorKind, localFiles, violations, reasons, visited = new Set() }) {
|
|
332
|
+
const ts = context.ts;
|
|
333
|
+
const classKey = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
|
|
334
|
+
if (visited.has(classKey)) return;
|
|
335
|
+
visited.add(classKey);
|
|
336
|
+
const properties = new Map(declaration.members.filter(member => ts.isPropertyDeclaration(member))
|
|
337
|
+
.map(member => [propertyName(ts, member), member]).filter(([name]) => name));
|
|
338
|
+
const state = { config, context, checker, declaration, classKind, properties, targets, decoratorKind,
|
|
339
|
+
framework: { targets, decoratorKind }, localFiles, violations, reasons, visited };
|
|
340
|
+
inspectInjectedMembers(state);
|
|
341
|
+
if (classKind) inspectInjectedBases(state);
|
|
342
|
+
}
|