@starci/hfs 4.0.10 → 4.2.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 +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -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/release-config.mjs +28 -0
- 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 +161 -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 +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -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 +164 -104
- 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 +121 -70
- 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/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- 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 +21 -4
- 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 +74 -695
- 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/env.mjs +10 -0
- 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/runtime/scripts/lib/yaml-cached.mjs +14 -0
- 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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isInside } from './config.mjs';
|
|
3
|
-
import { referencedExports, relativePath,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { referencedExports, relativePath, unwrapExpression } from './typescript.mjs';
|
|
4
|
+
import { commonJsRequireReasons, constructedDecoratorKind as sharedConstructedDecoratorKind, decoratorCallee, moduleExportsOf, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
|
|
5
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
6
|
+
import { hasAnyFlag } from '../../lib/ts-ast.mjs';
|
|
7
|
+
import { canonical, checkInjectedClass, checkMessageReadonly, isPublicMember, messageClass } from './contracts-readonly.mjs';
|
|
7
8
|
export const PUBLIC_CONTRACT_RULE_ID = 'BE_PUBLIC_CONTRACT_FORM';
|
|
8
|
-
export
|
|
9
|
+
export { READONLY_BOUNDARY_RULE_ID } from './contracts-readonly.mjs';
|
|
9
10
|
|
|
10
11
|
const FRAMEWORK_EXPORTS = new Map([
|
|
11
12
|
['@nestjs/common', new Set(['Controller', 'Inject', 'Injectable', 'Module'])],
|
|
@@ -17,12 +18,6 @@ const MESSAGE_HANDLERS = new Set(['CommandHandler', 'QueryHandler']);
|
|
|
17
18
|
const NON_API_SOURCE_ROLES = new Set(['config', 'configuration', 'constants', 'main', 'module', 'module-definition', 'providers']);
|
|
18
19
|
const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
|
|
19
20
|
|
|
20
|
-
function canonical(file) {
|
|
21
|
-
return path.resolve(file);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
21
|
function frameworkKindForSymbol(symbol, targets) {
|
|
27
22
|
const selected = targets.get(symbol);
|
|
28
23
|
if (selected) return selected;
|
|
@@ -43,23 +38,28 @@ const FRAMEWORK_TRACE = {
|
|
|
43
38
|
shorthandOf: (ts, checker, target, targets) => frameworkKindForSymbol(target, targets),
|
|
44
39
|
};
|
|
45
40
|
|
|
41
|
+
function addFrameworkTargets(config, context, checker, sourceFile, statement, bySymbol, reasons) {
|
|
42
|
+
const bound = moduleExportsOf(context.ts, checker, statement);
|
|
43
|
+
if (!bound || !FRAMEWORK_EXPORTS.has(bound.specifier)) return;
|
|
44
|
+
if (!bound.symbol) {
|
|
45
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${bound.specifier}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const name of referencedExports(context.ts, statement, FRAMEWORK_EXPORTS.get(bound.specifier))) {
|
|
49
|
+
const target = bound.exports.get(name);
|
|
50
|
+
if (target) bySymbol.set(target, name);
|
|
51
|
+
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${bound.specifier}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
function frameworkTargets(config, context, checker, localFiles) {
|
|
47
56
|
const bySymbol = new Map();
|
|
48
57
|
const reasons = [];
|
|
49
58
|
const files = programSourcesOf(context, checker, localFiles, { root: config.root });
|
|
50
59
|
if (!files) return { bySymbol, reasons: ['a TypeScript program checker could not be associated with its source'] };
|
|
51
60
|
for (const sourceFile of files) {
|
|
52
|
-
for (const statement of sourceFile.statements)
|
|
53
|
-
|
|
54
|
-
if (!bound || !FRAMEWORK_EXPORTS.has(bound.specifier)) continue;
|
|
55
|
-
if (!bound.symbol) { reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${bound.specifier}`); continue; }
|
|
56
|
-
for (const name of referencedExports(context.ts, statement, FRAMEWORK_EXPORTS.get(bound.specifier))) {
|
|
57
|
-
const target = bound.exports.get(name);
|
|
58
|
-
if (target) bySymbol.set(target, name);
|
|
59
|
-
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${bound.specifier}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
commonJsRequireReasons(context.ts, checker, sourceFile, (specifier) => FRAMEWORK_EXPORTS.has(specifier),
|
|
61
|
+
for (const statement of sourceFile.statements) addFrameworkTargets(config, context, checker, sourceFile, statement, bySymbol, reasons);
|
|
62
|
+
commonJsRequireReasons(context.ts, checker, sourceFile, specifier => FRAMEWORK_EXPORTS.has(specifier),
|
|
63
63
|
reasons, relativePath(config.root, sourceFile.fileName), 'binding whose contract role cannot be proved');
|
|
64
64
|
}
|
|
65
65
|
return { bySymbol, reasons };
|
|
@@ -73,36 +73,18 @@ function decoratorKind(ts, checker, decorator, targets) {
|
|
|
73
73
|
const constructedDecoratorKind = (ts, checker, decorator, targets) =>
|
|
74
74
|
sharedConstructedDecoratorKind(ts, checker, decorator, targets, FRAMEWORK_TRACE);
|
|
75
75
|
|
|
76
|
-
function modifiers(ts, node) {
|
|
77
|
-
return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? [];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function hasModifier(ts, node, kind) {
|
|
81
|
-
return modifiers(ts, node).some(modifier => modifier.kind === kind);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isPublicMember(ts, node) {
|
|
85
|
-
return !hasModifier(ts, node, ts.SyntaxKind.PrivateKeyword) && !hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function readonlyMember(ts, node) {
|
|
89
|
-
return hasModifier(ts, node, ts.SyntaxKind.ReadonlyKeyword);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
76
|
function sourceRole(sourceFile) {
|
|
95
77
|
const normalized = sourceFile.fileName.replaceAll('\\', '/');
|
|
96
78
|
const base = path.basename(sourceFile.fileName).replace(SOURCE_EXTENSION, '').toLowerCase();
|
|
97
79
|
const segments = base.split('.');
|
|
98
|
-
const role = segments.length > 1 ? segments.at(-1) : base === 'use-case' || base.endsWith('-use-case')
|
|
80
|
+
const role = segments.length > 1 ? segments.at(-1) : ((base === 'use-case' || base.endsWith('-use-case')) && 'use-case') || null;
|
|
99
81
|
return { base, role, transport: normalized.includes('/transport/') };
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
function isFrameworkHelper(sourceFile, declaration, framework) {
|
|
103
85
|
const role = sourceRole(sourceFile);
|
|
104
86
|
if (role.transport || NON_API_SOURCE_ROLES.has(role.role) || NON_API_SOURCE_ROLES.has(role.base)) return true;
|
|
105
|
-
if (declaration
|
|
87
|
+
if (declaration?.name && (declaration.name.text ?? '').endsWith('Module')) {
|
|
106
88
|
return nodeDecorators(framework.ts, declaration).some(decorator => decoratorKind(framework.ts, framework.checker, decorator, framework.targets) === 'Module');
|
|
107
89
|
}
|
|
108
90
|
return false;
|
|
@@ -115,65 +97,81 @@ function builtinSymbol(ts, symbol, name) {
|
|
|
115
97
|
&& /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
|
|
116
98
|
}
|
|
117
99
|
|
|
100
|
+
function typeReferenceStatus(ts, checker, node, seen, depth) {
|
|
101
|
+
const symbol = normalizedSymbol(ts, checker, node.typeName);
|
|
102
|
+
if (!symbol) return 'unavailable';
|
|
103
|
+
if (builtinSymbol(ts, symbol, 'Promise') || builtinSymbol(ts, symbol, 'Readonly') || builtinSymbol(ts, symbol, 'Awaited')
|
|
104
|
+
|| builtinSymbol(ts, symbol, 'Array') || builtinSymbol(ts, symbol, 'ReadonlyArray')) {
|
|
105
|
+
return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
|
|
106
|
+
}
|
|
107
|
+
if (builtinSymbol(ts, symbol, 'Record') || builtinSymbol(ts, symbol, 'Partial') || builtinSymbol(ts, symbol, 'Pick')
|
|
108
|
+
|| builtinSymbol(ts, symbol, 'Omit') || builtinSymbol(ts, symbol, 'Required')) return 'inline';
|
|
109
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
110
|
+
if (!declarations.length) return 'unavailable';
|
|
111
|
+
return declarations.every(declaration => ts.isTypeParameterDeclaration(declaration)
|
|
112
|
+
|| ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration)
|
|
113
|
+
|| ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) ? 'named' : 'unavailable';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isInlineContractType(ts, node) {
|
|
117
|
+
return ts.isTypeLiteralNode(node) || ts.isMappedTypeNode(node) || ts.isUnionTypeNode(node)
|
|
118
|
+
|| ts.isIntersectionTypeNode(node) || ts.isTupleTypeNode(node) || ts.isFunctionTypeNode(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isUnavailableContractType(ts, node) {
|
|
122
|
+
return ts.isConditionalTypeNode(node) || ts.isIndexedAccessTypeNode(node) || ts.isInferTypeNode(node)
|
|
123
|
+
|| ts.isTypeQueryNode(node) || ts.isImportTypeNode(node);
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
function contractTypeStatus(ts, checker, node, seen = new Set(), depth = 0) {
|
|
119
127
|
if (!node || depth > 12) return 'unavailable';
|
|
120
128
|
if (ts.isParenthesizedTypeNode(node)) return contractTypeStatus(ts, checker, node.type, seen, depth + 1);
|
|
121
129
|
if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
|
|
122
130
|
return contractTypeStatus(ts, checker, node.type, seen, depth + 1);
|
|
123
131
|
}
|
|
124
|
-
if (ts.isTypeReferenceNode(node))
|
|
125
|
-
const symbol = normalizedSymbol(ts, checker, node.typeName);
|
|
126
|
-
if (!symbol) return 'unavailable';
|
|
127
|
-
if (builtinSymbol(ts, symbol, 'Promise') || builtinSymbol(ts, symbol, 'Readonly') || builtinSymbol(ts, symbol, 'Awaited')) {
|
|
128
|
-
return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
|
|
129
|
-
}
|
|
130
|
-
if (builtinSymbol(ts, symbol, 'Array') || builtinSymbol(ts, symbol, 'ReadonlyArray')) {
|
|
131
|
-
return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
|
|
132
|
-
}
|
|
133
|
-
if (builtinSymbol(ts, symbol, 'Record') || builtinSymbol(ts, symbol, 'Partial') || builtinSymbol(ts, symbol, 'Pick')
|
|
134
|
-
|| builtinSymbol(ts, symbol, 'Omit') || builtinSymbol(ts, symbol, 'Required')) return 'inline';
|
|
135
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
136
|
-
if (!declarations.length) return 'unavailable';
|
|
137
|
-
return declarations.every(declaration => ts.isTypeParameterDeclaration(declaration)
|
|
138
|
-
|| ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration)
|
|
139
|
-
|| ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) ? 'named' : 'unavailable';
|
|
140
|
-
}
|
|
132
|
+
if (ts.isTypeReferenceNode(node)) return typeReferenceStatus(ts, checker, node, seen, depth);
|
|
141
133
|
if (ts.isArrayTypeNode(node)) return contractTypeStatus(ts, checker, node.elementType, seen, depth + 1);
|
|
142
|
-
if (ts
|
|
143
|
-
|
|
144
|
-
if (ts.isConditionalTypeNode(node) || ts.isIndexedAccessTypeNode(node) || ts.isInferTypeNode(node)
|
|
145
|
-
|| ts.isTypeQueryNode(node) || ts.isImportTypeNode(node)) return 'unavailable';
|
|
134
|
+
if (isInlineContractType(ts, node)) return 'inline';
|
|
135
|
+
if (isUnavailableContractType(ts, node)) return 'unavailable';
|
|
146
136
|
return 'scalar';
|
|
147
137
|
}
|
|
148
138
|
|
|
149
|
-
function
|
|
150
|
-
if (!type || depth > 12 || seen.has(type)) return 'unavailable';
|
|
151
|
-
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'unavailable';
|
|
152
|
-
const nextSeen = new Set(seen).add(type);
|
|
153
|
-
const named = type.aliasSymbol ?? type.symbol ?? null;
|
|
139
|
+
function wrappedTypeStatus(ts, checker, type, named, nextSeen, depth) {
|
|
154
140
|
for (const wrapper of ['Promise', 'Readonly', 'Awaited', 'Array', 'ReadonlyArray']) if (builtinSymbol(ts, named, wrapper)) {
|
|
155
|
-
const argumentsList = type.aliasTypeArguments ?? (checker.getTypeArguments && (type.objectFlags
|
|
141
|
+
const argumentsList = type.aliasTypeArguments ?? (checker.getTypeArguments && hasAnyFlag(type.objectFlags, ts.ObjectFlags.Reference)
|
|
156
142
|
? checker.getTypeArguments(type) : type.typeArguments) ?? [];
|
|
157
143
|
return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
|
|
158
144
|
}
|
|
159
145
|
for (const anonymous of ['Record', 'Partial', 'Pick', 'Omit', 'Required']) if (builtinSymbol(ts, named, anonymous)) return 'inline';
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function objectContractTypeStatus(ts, checker, type, nextSeen, depth) {
|
|
150
|
+
if (checker.isTupleType?.(type)) return 'inline';
|
|
151
|
+
if (checker.isArrayType?.(type)) {
|
|
152
|
+
const argumentsList = checker.getTypeArguments?.(type) ?? [];
|
|
153
|
+
return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
|
|
154
|
+
}
|
|
155
|
+
const declarations = type.symbol?.getDeclarations?.() ?? [];
|
|
156
|
+
if (type.symbol?.getName().startsWith('__') === false && declarations.some(declaration => ts.isInterfaceDeclaration(declaration)
|
|
157
|
+
|| ts.isClassDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration))) return 'named';
|
|
158
|
+
return 'inline';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function contractTypeStatusFromType(ts, checker, type, seen = new Set(), depth = 0) {
|
|
162
|
+
if (!type || depth > 12 || seen.has(type)) return 'unavailable';
|
|
163
|
+
if (hasAnyFlag(type.flags, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'unavailable';
|
|
164
|
+
const nextSeen = new Set(seen).add(type);
|
|
165
|
+
const named = type.aliasSymbol ?? type.symbol ?? null;
|
|
166
|
+
const wrapperStatus = wrappedTypeStatus(ts, checker, type, named, nextSeen, depth);
|
|
167
|
+
if (wrapperStatus !== null) return wrapperStatus;
|
|
160
168
|
if (type.aliasSymbol) return 'named';
|
|
161
169
|
if ((type.symbol?.getDeclarations?.() ?? []).some(declaration => ts.isEnumDeclaration(declaration))) return 'named';
|
|
162
|
-
if (type.flags
|
|
163
|
-
if (type.flags
|
|
164
|
-
if (type.flags
|
|
165
|
-
if (type.flags
|
|
166
|
-
|
|
167
|
-
if (checker.isArrayType?.(type)) {
|
|
168
|
-
const argumentsList = checker.getTypeArguments?.(type) ?? [];
|
|
169
|
-
return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
|
|
170
|
-
}
|
|
171
|
-
const declarations = type.symbol?.getDeclarations?.() ?? [];
|
|
172
|
-
if (type.symbol && !type.symbol.getName().startsWith('__') && declarations.some(declaration => ts.isInterfaceDeclaration(declaration)
|
|
173
|
-
|| ts.isClassDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration))) return 'named';
|
|
174
|
-
return 'inline';
|
|
175
|
-
}
|
|
176
|
-
if (type.flags & (ts.TypeFlags.Conditional | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) return 'unavailable';
|
|
170
|
+
if (hasAnyFlag(type.flags, ts.TypeFlags.TypeParameter)) return 'named';
|
|
171
|
+
if (hasAnyFlag(type.flags, ts.TypeFlags.Boolean)) return 'scalar';
|
|
172
|
+
if (hasAnyFlag(type.flags, ts.TypeFlags.Union | ts.TypeFlags.Intersection)) return 'inline';
|
|
173
|
+
if (hasAnyFlag(type.flags, ts.TypeFlags.Object)) return objectContractTypeStatus(ts, checker, type, nextSeen, depth);
|
|
174
|
+
if (hasAnyFlag(type.flags, ts.TypeFlags.Conditional | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) return 'unavailable';
|
|
177
175
|
return 'scalar';
|
|
178
176
|
}
|
|
179
177
|
|
|
@@ -184,9 +182,19 @@ function callableSignatures(ts, symbol, checker, location) {
|
|
|
184
182
|
return bodyless.length ? bodyless : signatures.filter(signature => signature.getDeclaration());
|
|
185
183
|
}
|
|
186
184
|
|
|
187
|
-
function
|
|
185
|
+
function signatureParameterStatus(ts, checker, signature, location, parameter, signatureIndex) {
|
|
186
|
+
const parameterSymbol = signature.getParameters()[signatureIndex];
|
|
187
|
+
const actual = parameterSymbol ? checker.getTypeOfSymbolAtLocation(parameterSymbol, location) : null;
|
|
188
|
+
if (actual && parameter.questionToken && !ts.isUnionTypeNode(parameter.type) && hasAnyFlag(actual.flags, ts.TypeFlags.Union)) {
|
|
189
|
+
const selected = actual.types.filter(type => !hasAnyFlag(type.flags, ts.TypeFlags.Undefined));
|
|
190
|
+
return selected.length === 1 ? contractTypeStatusFromType(ts, checker, selected[0]) : 'inline';
|
|
191
|
+
}
|
|
192
|
+
return actual ? contractTypeStatusFromType(ts, checker, actual) : contractTypeStatus(ts, checker, parameter.type);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function checkSignatureInputs(state, declaration) {
|
|
196
|
+
const { config, context, checker, signature, location, reportSource, reportNode, displayName, violations, reasons } = state;
|
|
188
197
|
const ts = context.ts;
|
|
189
|
-
const declaration = signature.getDeclaration();
|
|
190
198
|
let signatureIndex = 0;
|
|
191
199
|
for (let index = 0; index < (declaration.parameters?.length ?? 0); index += 1) {
|
|
192
200
|
const parameter = declaration.parameters[index];
|
|
@@ -197,18 +205,17 @@ function checkSignature(config, context, checker, signature, location, reportSou
|
|
|
197
205
|
signatureIndex += 1;
|
|
198
206
|
continue;
|
|
199
207
|
}
|
|
200
|
-
const
|
|
208
|
+
const status = signatureParameterStatus(ts, checker, signature, location, parameter, signatureIndex);
|
|
201
209
|
signatureIndex += 1;
|
|
202
|
-
const actual = parameterSymbol ? checker.getTypeOfSymbolAtLocation(parameterSymbol, location) : null;
|
|
203
|
-
let status;
|
|
204
|
-
if (actual && parameter.questionToken && !ts.isUnionTypeNode(parameter.type) && (actual.flags & ts.TypeFlags.Union)) {
|
|
205
|
-
const selected = actual.types.filter(type => !(type.flags & ts.TypeFlags.Undefined));
|
|
206
|
-
status = selected.length === 1 ? contractTypeStatusFromType(ts, checker, selected[0]) : 'inline';
|
|
207
|
-
} else status = actual ? contractTypeStatusFromType(ts, checker, actual) : contractTypeStatus(ts, checker, parameter.type);
|
|
208
210
|
if (status === 'inline') violations.push(violation(config, reportSource, reportNode ?? parameter.type, PUBLIC_CONTRACT_RULE_ID,
|
|
209
211
|
`${displayName} uses an inline object/union input; expose a named contract or a positional primitive.`));
|
|
210
212
|
else if (status === 'unavailable') reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot prove the public input type of ${displayName}`);
|
|
211
213
|
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function checkSignatureOutput(state, declaration) {
|
|
217
|
+
const { config, context, checker, signature, reportSource, reportNode, displayName, violations, reasons } = state;
|
|
218
|
+
const ts = context.ts;
|
|
212
219
|
if (!declaration.type) {
|
|
213
220
|
violations.push(violation(config, reportSource, reportNode ?? declaration, PUBLIC_CONTRACT_RULE_ID,
|
|
214
221
|
`${displayName} must declare an explicit public output type.`));
|
|
@@ -221,6 +228,12 @@ function checkSignature(config, context, checker, signature, location, reportSou
|
|
|
221
228
|
else if (status === 'unavailable') reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot prove the public output type of ${displayName}`);
|
|
222
229
|
}
|
|
223
230
|
|
|
231
|
+
function checkSignature(state) {
|
|
232
|
+
const declaration = state.signature.getDeclaration();
|
|
233
|
+
checkSignatureInputs(state, declaration);
|
|
234
|
+
checkSignatureOutput(state, declaration);
|
|
235
|
+
}
|
|
236
|
+
|
|
224
237
|
function symbolName(symbol) {
|
|
225
238
|
const name = symbol.getName?.() ?? '(anonymous)';
|
|
226
239
|
return name === '__call' ? '(callable)' : name;
|
|
@@ -230,6 +243,17 @@ function isRepositoryDeclaration(declaration, localFiles) {
|
|
|
230
243
|
return localFiles.has(canonical(declaration.getSourceFile().fileName));
|
|
231
244
|
}
|
|
232
245
|
|
|
246
|
+
const isPublicCallableDeclaration = (ts, declaration) => ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)
|
|
247
|
+
|| ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration) || ts.isGetAccessorDeclaration(declaration);
|
|
248
|
+
|
|
249
|
+
function isCallableMember(ts, checker, member, location, localFiles) {
|
|
250
|
+
if (member.getName() === 'prototype') return false;
|
|
251
|
+
const selected = member.getDeclarations?.() ?? [];
|
|
252
|
+
return selected.some(declaration => isPublicCallableDeclaration(ts, declaration)
|
|
253
|
+
&& isPublicMember(ts, declaration) && isRepositoryDeclaration(declaration, localFiles))
|
|
254
|
+
&& checker.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(member, location), ts.SignatureKind.Call).length;
|
|
255
|
+
}
|
|
256
|
+
|
|
233
257
|
function callableMembers(ts, checker, symbol, location, localFiles) {
|
|
234
258
|
const declarations = symbol.getDeclarations?.() ?? [];
|
|
235
259
|
const classExpressions = declarations.flatMap(declaration => ts.isVariableDeclaration(declaration)
|
|
@@ -242,14 +266,7 @@ function callableMembers(ts, checker, symbol, location, localFiles) {
|
|
|
242
266
|
if (declaredClassLike || classExpressions.length) types.push(valueType);
|
|
243
267
|
for (const signature of checker.getSignaturesOfType(valueType, ts.SignatureKind.Construct)) types.push(checker.getReturnTypeOfSignature(signature));
|
|
244
268
|
const direct = checker.getSignaturesOfType(primary, ts.SignatureKind.Call).length ? [symbol] : [];
|
|
245
|
-
const members = types.flatMap(type => checker.getPropertiesOfType(type)).filter(member =>
|
|
246
|
-
if (member.getName() === 'prototype') return false;
|
|
247
|
-
const selected = member.getDeclarations?.() ?? [];
|
|
248
|
-
return selected.some(declaration => (ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)
|
|
249
|
-
|| ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration) || ts.isGetAccessorDeclaration(declaration))
|
|
250
|
-
&& isPublicMember(ts, declaration) && isRepositoryDeclaration(declaration, localFiles))
|
|
251
|
-
&& checker.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(member, location), ts.SignatureKind.Call).length;
|
|
252
|
-
});
|
|
269
|
+
const members = types.flatMap(type => checker.getPropertiesOfType(type)).filter(member => isCallableMember(ts, checker, member, location, localFiles));
|
|
253
270
|
return [...new Set([...direct, ...members])];
|
|
254
271
|
}
|
|
255
272
|
|
|
@@ -258,7 +275,7 @@ function exportedSymbols(ts, checker, sourceFile) {
|
|
|
258
275
|
return moduleSymbol ? checker.getExportsOfModule(moduleSymbol).map(symbol => normalizedSymbolValue(ts, checker, symbol)).filter(Boolean) : [];
|
|
259
276
|
}
|
|
260
277
|
|
|
261
|
-
function checkCallableSymbol(config, context, checker, symbol, location, reportSource, reportNode, violations, reasons, seen, localFiles) {
|
|
278
|
+
function checkCallableSymbol({ config, context, checker, symbol, location, reportSource, reportNode, violations, reasons, seen, localFiles }) {
|
|
262
279
|
if (seen.has(symbol)) return 0;
|
|
263
280
|
seen.add(symbol);
|
|
264
281
|
let checked = 0;
|
|
@@ -272,392 +289,183 @@ function checkCallableSymbol(config, context, checker, symbol, location, reportS
|
|
|
272
289
|
}
|
|
273
290
|
for (const signature of signatures) {
|
|
274
291
|
checked += 1;
|
|
275
|
-
checkSignature(config, context, checker, signature, location, reportSource, reportNode,
|
|
276
|
-
callable === symbol ? symbolName(symbol) : `${symbolName(symbol)}.${symbolName(callable)}`,
|
|
277
|
-
violations, reasons);
|
|
292
|
+
checkSignature({ config, context, checker, signature, location, reportSource, reportNode,
|
|
293
|
+
displayName: callable === symbol ? symbolName(symbol) : `${symbolName(symbol)}.${symbolName(callable)}`,
|
|
294
|
+
violations, reasons });
|
|
278
295
|
}
|
|
279
296
|
}
|
|
280
297
|
return checked;
|
|
281
298
|
}
|
|
282
299
|
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
function thisOriginStatus(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
290
|
-
if (!expression || depth > 8) return 'no';
|
|
291
|
-
expression = unwrapExpression(ts, expression);
|
|
292
|
-
if (expression.kind === ts.SyntaxKind.ThisKeyword) return 'yes';
|
|
293
|
-
const symbol = normalizedSymbol(ts, checker, expression);
|
|
294
|
-
if (!symbol || seen.has(symbol)) return 'no';
|
|
295
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
296
|
-
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer) return 'no';
|
|
297
|
-
const origin = thisOriginStatus(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
|
|
298
|
-
if (origin === 'no') return 'no';
|
|
299
|
-
return (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const) ? origin : 'unavailable';
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function instanceAssignmentTarget(ts, checker, expression) {
|
|
303
|
-
expression = unwrapExpression(ts, expression);
|
|
304
|
-
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return { instance: false };
|
|
305
|
-
const owner = thisOriginStatus(ts, checker, expression.expression);
|
|
306
|
-
if (owner === 'no') return { instance: false };
|
|
307
|
-
if (owner === 'unavailable') return { instance: true, name: null, supported: false };
|
|
308
|
-
if (ts.isPropertyAccessExpression(expression)) return { instance: true, name: expression.name.text, supported: true };
|
|
309
|
-
if (ts.isStringLiteralLike(expression.argumentExpression)) return { instance: true, name: expression.argumentExpression.text, supported: true };
|
|
310
|
-
return { instance: true, name: null, supported: false };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function expressionOrigin(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
314
|
-
if (!expression || depth > 8) return null;
|
|
315
|
-
expression = unwrapExpression(ts, expression);
|
|
316
|
-
const shorthand = ts.isShorthandPropertyAssignment(expression) ? checker.getShorthandAssignmentValueSymbol?.(expression) : null;
|
|
317
|
-
const symbol = normalizedSymbolValue(ts, checker, shorthand) ?? normalizedSymbol(ts, checker, expression);
|
|
318
|
-
if (!symbol || seen.has(symbol)) return symbol;
|
|
319
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
320
|
-
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
321
|
-
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
322
|
-
return expressionOrigin(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
|
|
300
|
+
function frameworkFor(state, checker) {
|
|
301
|
+
if (!state.frameworkByChecker.has(checker)) {
|
|
302
|
+
const selected = frameworkTargets(state.config, state.context, checker, state.localFiles);
|
|
303
|
+
state.frameworkByChecker.set(checker, { ...selected, ts: state.ts, checker, targets: selected.bySymbol });
|
|
304
|
+
state.readonlyReasons.push(...selected.reasons);
|
|
323
305
|
}
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const expressionReferencesOrigin = (ts, checker, expression, expected) =>
|
|
328
|
-
anyDescendant(ts, expression, (node) => expressionOrigin(ts, checker, node) === expected);
|
|
329
|
-
|
|
330
|
-
function isSafePrimitiveProjection(ts, checker, expression, expected) {
|
|
331
|
-
expression = unwrapExpression(ts, expression);
|
|
332
|
-
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false;
|
|
333
|
-
if (expressionOrigin(ts, checker, expression.expression) !== expected) return false;
|
|
334
|
-
const type = checker.getTypeAtLocation(expression);
|
|
335
|
-
const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike
|
|
336
|
-
| ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
337
|
-
| ts.TypeFlags.Void | ts.TypeFlags.Never;
|
|
338
|
-
return Boolean(type.flags & primitive) && !(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function assignmentPatternHasInstanceTarget(ts, checker, expression) {
|
|
342
|
-
let found = false;
|
|
343
|
-
const visit = node => {
|
|
344
|
-
if (found) return;
|
|
345
|
-
const target = instanceAssignmentTarget(ts, checker, node);
|
|
346
|
-
if (target.instance) {
|
|
347
|
-
found = true;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
ts.forEachChild(node, visit);
|
|
351
|
-
};
|
|
352
|
-
visit(expression);
|
|
353
|
-
return found;
|
|
306
|
+
return state.frameworkByChecker.get(checker);
|
|
354
307
|
}
|
|
355
308
|
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
309
|
+
function checkOwnerCoverage(state) {
|
|
310
|
+
const { config, context, publicReasons } = state;
|
|
311
|
+
if (!config.owners?.length) publicReasons.push('no slot owner with an entry file exists, so capability API discovery has no owners to list');
|
|
312
|
+
const ownerRoots = (config.owners ?? []).map(owner => path.resolve(config.root, ...owner.root.split('/')));
|
|
313
|
+
const publicSourceRoots = [...config.backend.features, ...config.backend.modules].map(root => path.resolve(config.root, ...root.split('/')));
|
|
314
|
+
for (const sourceFile of context.files) if (publicSourceRoots.some(root => isInside(root, sourceFile.fileName))
|
|
315
|
+
&& !ownerRoots.some(root => isInside(root, sourceFile.fileName))) {
|
|
316
|
+
publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} is outside every declared owner root`);
|
|
317
|
+
}
|
|
362
318
|
}
|
|
363
319
|
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function constructorAssignments(ts, checker, constructor, parameter) {
|
|
375
|
-
if (!constructor.body) return [];
|
|
376
|
-
const parameterSymbol = normalizedSymbol(ts, checker, parameter.name);
|
|
377
|
-
const assignments = [];
|
|
378
|
-
const visit = node => {
|
|
379
|
-
if (node !== constructor.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
380
|
-
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
|
|
381
|
-
if (ts.isBinaryExpression(node) && expressionReferencesOrigin(ts, checker, node.right, parameterSymbol)
|
|
382
|
-
&& !isSafePrimitiveProjection(ts, checker, node.right, parameterSymbol)) {
|
|
383
|
-
const target = instanceAssignmentTarget(ts, checker, node.left);
|
|
384
|
-
if (target.instance) assignments.push({ node, name: target.name,
|
|
385
|
-
supported: target.supported && node.operatorToken.kind === ts.SyntaxKind.EqualsToken });
|
|
386
|
-
else if (assignmentPatternHasInstanceTarget(ts, checker, node.left)) assignments.push({ node, name: null, supported: false });
|
|
387
|
-
else ts.forEachChild(node, visit);
|
|
388
|
-
} else {
|
|
389
|
-
const mutation = instanceMutationCall(ts, checker, node);
|
|
390
|
-
if (mutation?.values.some(argument => expressionReferencesOrigin(ts, checker, argument, parameterSymbol))) {
|
|
391
|
-
assignments.push({ node, name: null, supported: false });
|
|
392
|
-
} else ts.forEachChild(node, visit);
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
visit(constructor.body);
|
|
396
|
-
return assignments;
|
|
320
|
+
function checkExportedSymbol(state, checker, framework, symbol) {
|
|
321
|
+
const { context, localFiles, publicReasons, violations, publicSeen } = state;
|
|
322
|
+
const declarations = (symbol.getDeclarations?.() ?? []).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
|
|
323
|
+
if (!declarations.length) return;
|
|
324
|
+
const representative = declarations[0];
|
|
325
|
+
if (isFrameworkHelper(representative.getSourceFile(), representative, framework)) return;
|
|
326
|
+
state.callableSignatures += checkCallableSymbol({ config: state.config, context, checker, symbol, location: representative,
|
|
327
|
+
reportSource: representative.getSourceFile(), reportNode: representative.name ?? representative,
|
|
328
|
+
violations, reasons: publicReasons, seen: publicSeen, localFiles });
|
|
397
329
|
}
|
|
398
330
|
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
331
|
+
function checkOwnerEntry(state, owner) {
|
|
332
|
+
const { config, context, ts, sourceFiles, publicReasons } = state;
|
|
333
|
+
const entry = sourceFiles.get(canonical(path.resolve(config.root, ...owner.entry.split('/'))));
|
|
334
|
+
if (!entry) {
|
|
335
|
+
publicReasons.push(`${owner.entry} is outside the checked production program`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const checker = context.checkerFor(entry.fileName);
|
|
339
|
+
const framework = frameworkFor(state, checker);
|
|
340
|
+
if (entry.statements.some(statement => ts.isExportAssignment(statement) && statement.isExportEquals)) {
|
|
341
|
+
publicReasons.push(`${owner.entry} uses export =, so named capability API discovery is unavailable`);
|
|
342
|
+
}
|
|
343
|
+
for (const symbol of exportedSymbols(ts, checker, entry)) checkExportedSymbol(state, checker, framework, symbol);
|
|
402
344
|
}
|
|
403
345
|
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
if (ts.isClassDeclaration(declaration)) return [declaration];
|
|
407
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
408
|
-
const initializer = unwrapExpression(ts, declaration.initializer);
|
|
409
|
-
if (ts.isClassExpression(initializer)) return [initializer];
|
|
410
|
-
}
|
|
411
|
-
return [];
|
|
412
|
-
}).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
|
|
346
|
+
function checkPublicOwners(state) {
|
|
347
|
+
for (const owner of state.config.owners ?? []) checkOwnerEntry(state, owner);
|
|
413
348
|
}
|
|
414
349
|
|
|
415
|
-
function
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (declarations.length !== 1) {
|
|
424
|
-
reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} cannot resolve one local message class from its handler decorator`);
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
return declarations[0];
|
|
350
|
+
function classesInSource(ts, sourceFile) {
|
|
351
|
+
const classes = [];
|
|
352
|
+
const collect = node => {
|
|
353
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) classes.push(node);
|
|
354
|
+
ts.forEachChild(node, collect);
|
|
355
|
+
};
|
|
356
|
+
collect(sourceFile);
|
|
357
|
+
return classes;
|
|
428
358
|
}
|
|
429
359
|
|
|
430
|
-
function
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (visited.has(key)) return;
|
|
436
|
-
visited.add(key);
|
|
437
|
-
const sourceFile = selected.getSourceFile();
|
|
438
|
-
for (const member of selected.members) {
|
|
439
|
-
if (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword)) {
|
|
440
|
-
checkReadonlyProperty(config, context, sourceFile, member, `Message field ${propertyName(ts, member) ?? '(computed)'}`, violations);
|
|
441
|
-
}
|
|
442
|
-
if (ts.isSetAccessorDeclaration(member)) violations.push(violation(config, sourceFile, member.name, READONLY_BOUNDARY_RULE_ID,
|
|
443
|
-
`Message field ${propertyName(ts, member) ?? '(computed)'} cannot expose a setter.`));
|
|
444
|
-
if (ts.isIndexSignatureDeclaration(member)) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has a message index signature whose mutation boundary cannot be proved`);
|
|
445
|
-
if (ts.isConstructorDeclaration(member)) {
|
|
446
|
-
for (const parameter of member.parameters) if (modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword,
|
|
447
|
-
ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind))) {
|
|
448
|
-
checkReadonlyProperty(config, context, sourceFile, parameter, `Message field ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
|
|
449
|
-
}
|
|
450
|
-
const inspectAssign = node => {
|
|
451
|
-
if (node !== member.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
|
|
452
|
-
if (instanceMutationCall(ts, checker, node)) {
|
|
453
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance mutation`);
|
|
454
|
-
} else if (ts.isBinaryExpression(node) && assignmentPatternHasInstanceTarget(ts, checker, node.left)
|
|
455
|
-
&& !instanceAssignmentTarget(ts, checker, node.left).supported) {
|
|
456
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance assignment`);
|
|
457
|
-
} else ts.forEachChild(node, inspectAssign);
|
|
458
|
-
};
|
|
459
|
-
if (member.body) inspectAssign(member.body);
|
|
460
|
-
}
|
|
360
|
+
function checkConstructedClassDecorators(state, sourceFile, checker, decorators, kinds, targets) {
|
|
361
|
+
for (const [index, decorator] of decorators.entries()) if (!kinds[index]) {
|
|
362
|
+
const constructed = constructedDecoratorKind(state.ts, checker, decorator, targets);
|
|
363
|
+
if (constructed && (NEST_CREATED.has(constructed) || MESSAGE_HANDLERS.has(constructed) || constructed.includes('framework'))) {
|
|
364
|
+
state.readonlyReasons.push(`${relativePath(state.config.root, sourceFile.fileName)} has a constructed ${constructed} decorator identity`);
|
|
461
365
|
}
|
|
462
|
-
|
|
463
|
-
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
464
|
-
const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
465
|
-
if (declarations.length !== 1) {
|
|
466
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
|
|
467
|
-
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) visitClass(declarations[0]);
|
|
468
|
-
else if (classCarriesState(ts, declarations[0])) {
|
|
469
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message state from a class outside the checked production program`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
visitClass(declaration);
|
|
366
|
+
}
|
|
474
367
|
}
|
|
475
368
|
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
369
|
+
function checkConstructedMemberDecorator(state, sourceFile, checker, decorator, targets) {
|
|
370
|
+
const { ts, config, readonlyReasons } = state;
|
|
371
|
+
if (decoratorKind(ts, checker, decorator, targets)) return;
|
|
372
|
+
const constructed = constructedDecoratorKind(ts, checker, decorator, targets);
|
|
373
|
+
if (constructed === 'Inject' || constructed === 'unproven framework') {
|
|
374
|
+
readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} injection decorator identity`);
|
|
375
|
+
}
|
|
480
376
|
}
|
|
481
377
|
|
|
482
|
-
function
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return constructor ? checker.getReturnTypeOfSignature(constructor) : value;
|
|
378
|
+
function checkConstructedMemberDecorators(state, sourceFile, declaration, checker, targets) {
|
|
379
|
+
const { ts } = state;
|
|
380
|
+
for (const member of declaration.members) for (const decorated of [member, ...(ts.isConstructorDeclaration(member) ? member.parameters : [])]) {
|
|
381
|
+
for (const decorator of nodeDecorators(ts, decorated)) checkConstructedMemberDecorator(state, sourceFile, checker, decorator, targets);
|
|
382
|
+
}
|
|
488
383
|
}
|
|
489
384
|
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const parameterProperty = modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
|
|
509
|
-
ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind));
|
|
510
|
-
if (parameterProperty) {
|
|
511
|
-
checkReadonlyProperty(config, context, sourceFile, parameter, `Injected dependency ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
for (const assignment of constructorAssignments(ts, checker, member, parameter)) {
|
|
515
|
-
if (!assignment.supported || !assignment.name) {
|
|
516
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)}:${sourceLocation(sourceFile, assignment.node).line} stores an injected dependency through an unsupported instance assignment`);
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
const property = properties.get(assignment.name);
|
|
520
|
-
if (!property) reasons.push(`${relativePath(config.root, sourceFile.fileName)} assigns injected dependency ${assignment.name} without a declared field`);
|
|
521
|
-
else checkReadonlyProperty(config, context, sourceFile, property, `Injected dependency ${assignment.name}`, violations);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (classKind) {
|
|
526
|
-
const type = classInstanceType(ts, checker, declaration);
|
|
527
|
-
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
528
|
-
const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
529
|
-
if (declarations.length !== 1) {
|
|
530
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
531
|
-
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
|
|
532
|
-
checkInjectedClass(config, context, checker, declarations[0], classKind, targets, localFiles, violations, reasons, visited);
|
|
533
|
-
} else if (classCarriesState(ts, declarations[0])) {
|
|
534
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
385
|
+
function hasInjectedMember(ts, checker, declaration, targets) {
|
|
386
|
+
return declaration.members.some(member => nodeDecorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject')
|
|
387
|
+
|| (ts.isConstructorDeclaration(member) && member.parameters.some(parameter => nodeDecorators(ts, parameter)
|
|
388
|
+
.some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject'))));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function checkMessageHandlers(state, sourceFile, checker, decorators, kinds) {
|
|
392
|
+
for (const [index, decorator] of decorators.entries()) {
|
|
393
|
+
const kind = kinds[index];
|
|
394
|
+
if (!MESSAGE_HANDLERS.has(kind)) continue;
|
|
395
|
+
const message = messageClass(state.config, state.context, checker, decorator, state.localFiles, state.readonlyReasons);
|
|
396
|
+
if (!message) continue;
|
|
397
|
+
const key = `${message.getSourceFile().fileName}:${message.pos}`;
|
|
398
|
+
if (state.messageSeen.has(key)) continue;
|
|
399
|
+
state.messageSeen.add(key);
|
|
400
|
+
state.messages += 1;
|
|
401
|
+
checkMessageReadonly(state.config, state.context, state.context.checkerFor(message.getSourceFile().fileName), message,
|
|
402
|
+
state.localFiles, state.violations, state.readonlyReasons);
|
|
537
403
|
}
|
|
538
404
|
}
|
|
539
405
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const publicSourceRoots = [...config.backend.features, ...config.backend.modules].map(root => path.resolve(config.root, ...root.split('/')));
|
|
567
|
-
for (const sourceFile of context.files) if (publicSourceRoots.some(root => isInside(root, sourceFile.fileName))
|
|
568
|
-
&& !ownerRoots.some(root => isInside(root, sourceFile.fileName))) {
|
|
569
|
-
publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} is outside every declared owner root`);
|
|
570
|
-
}
|
|
571
|
-
if (config.owners) for (const owner of config.owners) {
|
|
572
|
-
const entry = sourceFiles.get(canonical(path.resolve(config.root, ...owner.entry.split('/'))));
|
|
573
|
-
if (!entry) {
|
|
574
|
-
publicReasons.push(`${owner.entry} is outside the checked production program`);
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
const checker = context.checkerFor(entry.fileName);
|
|
578
|
-
const framework = frameworkFor(checker);
|
|
579
|
-
if (entry.statements.some(statement => ts.isExportAssignment(statement) && statement.isExportEquals)) {
|
|
580
|
-
publicReasons.push(`${owner.entry} uses export =, so named capability API discovery is unavailable`);
|
|
581
|
-
}
|
|
582
|
-
for (const symbol of exportedSymbols(ts, checker, entry)) {
|
|
583
|
-
const declarations = (symbol.getDeclarations?.() ?? []).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
|
|
584
|
-
if (!declarations.length) continue;
|
|
585
|
-
const representative = declarations[0];
|
|
586
|
-
if (isFrameworkHelper(representative.getSourceFile(), representative, framework)) continue;
|
|
587
|
-
callableSignatures += checkCallableSymbol(config, context, checker, symbol, representative, representative.getSourceFile(), representative.name ?? representative,
|
|
588
|
-
violations, publicReasons, publicSeen, localFiles);
|
|
589
|
-
}
|
|
406
|
+
function checkUseCase(state, role, sourceFile, checker, declaration) {
|
|
407
|
+
const { ts, config, context, publicReasons, publicSeen, localFiles, violations } = state;
|
|
408
|
+
if (!(role.role === 'use-case' && !role.transport && ts.isClassDeclaration(declaration) && declaration.name)) return;
|
|
409
|
+
const exported = exportedSymbols(ts, checker, sourceFile).some(symbol => (symbol.getDeclarations?.() ?? []).includes(declaration));
|
|
410
|
+
if (!exported) return;
|
|
411
|
+
const classSymbol = normalizedSymbol(ts, checker, declaration.name);
|
|
412
|
+
const instanceType = classSymbol ? checker.getDeclaredTypeOfSymbol(classSymbol) : null;
|
|
413
|
+
const execute = instanceType ? checker.getPropertyOfType(instanceType, 'execute') : null;
|
|
414
|
+
if (!execute) publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} exports a use-case class without a statically resolved execute contract`);
|
|
415
|
+
else state.callableSignatures += checkCallableSymbol({ config, context, checker, symbol: execute, location: declaration,
|
|
416
|
+
reportSource: sourceFile, reportNode: declaration.name, violations, reasons: publicReasons, seen: publicSeen, localFiles });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function checkClassContracts(state, sourceFile, checker, role, declaration, framework) {
|
|
420
|
+
const { ts, config, context, violations, readonlyReasons, localFiles } = state;
|
|
421
|
+
const decorators = nodeDecorators(ts, declaration);
|
|
422
|
+
const kinds = decorators.map(decorator => decoratorKind(ts, checker, decorator, framework.targets));
|
|
423
|
+
const directKinds = kinds.filter(Boolean);
|
|
424
|
+
checkConstructedClassDecorators(state, sourceFile, checker, decorators, kinds, framework.targets);
|
|
425
|
+
const classKind = directKinds.find(kind => NEST_CREATED.has(kind)) ?? null;
|
|
426
|
+
const targets = framework.targets;
|
|
427
|
+
checkConstructedMemberDecorators(state, sourceFile, declaration, checker, targets);
|
|
428
|
+
if (classKind || hasInjectedMember(ts, checker, declaration, targets)) {
|
|
429
|
+
state.injectedClasses += 1;
|
|
430
|
+
checkInjectedClass({ config, context, checker, declaration, classKind, targets, decoratorKind,
|
|
431
|
+
localFiles, violations, reasons: readonlyReasons });
|
|
590
432
|
}
|
|
433
|
+
checkMessageHandlers(state, sourceFile, checker, decorators, kinds);
|
|
434
|
+
checkUseCase(state, role, sourceFile, checker, declaration);
|
|
435
|
+
}
|
|
591
436
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) classes.push(node);
|
|
599
|
-
ts.forEachChild(node, collectClasses);
|
|
600
|
-
};
|
|
601
|
-
collectClasses(sourceFile);
|
|
602
|
-
for (const statement of classes) {
|
|
603
|
-
const classDecorators = nodeDecorators(ts, statement);
|
|
604
|
-
const kindsByDecorator = classDecorators.map(decorator => decoratorKind(ts, checker, decorator, framework.targets));
|
|
605
|
-
const directKinds = kindsByDecorator.filter(Boolean);
|
|
606
|
-
for (const [index, decorator] of classDecorators.entries()) if (!kindsByDecorator[index]) {
|
|
607
|
-
const constructed = constructedDecoratorKind(ts, checker, decorator, framework.targets);
|
|
608
|
-
if (constructed && (NEST_CREATED.has(constructed) || MESSAGE_HANDLERS.has(constructed) || constructed.includes('framework'))) {
|
|
609
|
-
readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} decorator identity`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
const classKind = directKinds.find(kind => NEST_CREATED.has(kind)) ?? null;
|
|
613
|
-
for (const member of statement.members) for (const decorated of [member, ...(ts.isConstructorDeclaration(member) ? member.parameters : [])]) {
|
|
614
|
-
for (const decorator of nodeDecorators(ts, decorated)) if (!decoratorKind(ts, checker, decorator, framework.targets)) {
|
|
615
|
-
const constructed = constructedDecoratorKind(ts, checker, decorator, framework.targets);
|
|
616
|
-
if (constructed === 'Inject' || constructed === 'unproven framework') {
|
|
617
|
-
readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} injection decorator identity`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const hasInjectedMember = statement.members.some(member => nodeDecorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, framework.targets) === 'Inject')
|
|
622
|
-
|| (ts.isConstructorDeclaration(member) && member.parameters.some(parameter => nodeDecorators(ts, parameter)
|
|
623
|
-
.some(decorator => decoratorKind(ts, checker, decorator, framework.targets) === 'Inject'))));
|
|
624
|
-
if (classKind || hasInjectedMember) {
|
|
625
|
-
injectedClasses += 1;
|
|
626
|
-
checkInjectedClass(config, context, checker, statement, classKind, framework.targets, localFiles, violations, readonlyReasons);
|
|
627
|
-
}
|
|
628
|
-
for (let index = 0; index < classDecorators.length; index += 1) {
|
|
629
|
-
const kind = kindsByDecorator[index];
|
|
630
|
-
if (!MESSAGE_HANDLERS.has(kind)) continue;
|
|
631
|
-
const message = messageClass(config, context, checker, classDecorators[index], localFiles, readonlyReasons);
|
|
632
|
-
if (!message) continue;
|
|
633
|
-
const key = `${message.getSourceFile().fileName}:${message.pos}`;
|
|
634
|
-
if (messageSeen.has(key)) continue;
|
|
635
|
-
messageSeen.add(key);
|
|
636
|
-
messages += 1;
|
|
637
|
-
checkMessageReadonly(config, context, context.checkerFor(message.getSourceFile().fileName), message, localFiles, violations, readonlyReasons);
|
|
638
|
-
}
|
|
639
|
-
if (role.role === 'use-case' && !role.transport && ts.isClassDeclaration(statement) && statement.name) {
|
|
640
|
-
const exported = exportedSymbols(ts, checker, sourceFile).some(symbol => (symbol.getDeclarations?.() ?? []).includes(statement));
|
|
641
|
-
if (!exported) continue;
|
|
642
|
-
const classSymbol = normalizedSymbol(ts, checker, statement.name);
|
|
643
|
-
const instanceType = classSymbol ? checker.getDeclaredTypeOfSymbol(classSymbol) : null;
|
|
644
|
-
const execute = instanceType ? checker.getPropertyOfType(instanceType, 'execute') : null;
|
|
645
|
-
if (!execute) publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} exports a use-case class without a statically resolved execute contract`);
|
|
646
|
-
else callableSignatures += checkCallableSymbol(config, context, checker, execute, statement, sourceFile, statement.name,
|
|
647
|
-
violations, publicReasons, publicSeen, localFiles);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
437
|
+
function checkSourceFile(state, sourceFile) {
|
|
438
|
+
const checker = state.context.checkerFor(sourceFile.fileName);
|
|
439
|
+
const framework = frameworkFor(state, checker);
|
|
440
|
+
const role = sourceRole(sourceFile);
|
|
441
|
+
for (const declaration of classesInSource(state.ts, sourceFile)) checkClassContracts(state, sourceFile, checker, role, declaration, framework);
|
|
442
|
+
}
|
|
651
443
|
|
|
652
|
-
|
|
653
|
-
const
|
|
444
|
+
function contractCoverage(state) {
|
|
445
|
+
const publicDetails = [...new Set(state.publicReasons)].sort(byCodeUnit);
|
|
446
|
+
const readonlyDetails = [...new Set(state.readonlyReasons)].sort(byCodeUnit);
|
|
654
447
|
return {
|
|
655
|
-
violations,
|
|
448
|
+
violations: state.violations,
|
|
656
449
|
coverage: {
|
|
657
|
-
publicContracts: publicDetails.length ? { status: 'unavailable', callableSignatures, details: publicDetails }
|
|
658
|
-
: { status: 'checked', callableSignatures },
|
|
659
|
-
readonlyBoundaries: readonlyDetails.length ? { status: 'unavailable', injectedClasses, messages, details: readonlyDetails }
|
|
660
|
-
: { status: 'checked', injectedClasses, messages },
|
|
450
|
+
publicContracts: publicDetails.length ? { status: 'unavailable', callableSignatures: state.callableSignatures, details: publicDetails }
|
|
451
|
+
: { status: 'checked', callableSignatures: state.callableSignatures },
|
|
452
|
+
readonlyBoundaries: readonlyDetails.length ? { status: 'unavailable', injectedClasses: state.injectedClasses, messages: state.messages, details: readonlyDetails }
|
|
453
|
+
: { status: 'checked', injectedClasses: state.injectedClasses, messages: state.messages },
|
|
661
454
|
},
|
|
662
455
|
};
|
|
663
456
|
}
|
|
457
|
+
|
|
458
|
+
/** Enforce mechanically selected public contract and readonly boundaries without treating DTOs or arbitrary interfaces as APIs. */
|
|
459
|
+
export function checkBackendContracts(config, context) {
|
|
460
|
+
const state = {
|
|
461
|
+
config, context, ts: context.ts,
|
|
462
|
+
localFiles: new Set(context.files.map(file => canonical(file.fileName))),
|
|
463
|
+
sourceFiles: new Map(context.files.map(file => [canonical(file.fileName), file])),
|
|
464
|
+
frameworkByChecker: new Map(), violations: [], publicReasons: [], readonlyReasons: [],
|
|
465
|
+
publicSeen: new Set(), messageSeen: new Set(), callableSignatures: 0, injectedClasses: 0, messages: 0,
|
|
466
|
+
};
|
|
467
|
+
checkOwnerCoverage(state);
|
|
468
|
+
checkPublicOwners(state);
|
|
469
|
+
for (const sourceFile of context.files) checkSourceFile(state, sourceFile);
|
|
470
|
+
return contractCoverage(state);
|
|
471
|
+
}
|