@starci/hfs 1.0.0 → 2.0.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 +57 -0
- package/README.md +116 -12
- package/bin/hfs.mjs +119 -15
- package/emit/compiler.mjs +35 -0
- package/emit/contracts.mjs +97 -0
- package/emit/operations-worker.mjs +24 -0
- package/emit/operations.mjs +126 -0
- package/emit/schema-worker.mjs +117 -0
- package/emit/static-graph.mjs +670 -0
- package/emit/type-schema.mjs +145 -0
- package/package.json +10 -2
- package/report/sonar.mjs +180 -0
- package/runtime/engine/admission.mjs +284 -0
- package/runtime/engine/digest.mjs +10 -0
- package/runtime/engine/ledger-db.mjs +1245 -0
- package/runtime/engine/machine-db.mjs +1484 -0
- package/runtime/engine/migrations/machine/0001-init.sql +887 -0
- package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
- package/runtime/knowledge/hfs/slots.yaml +409 -140
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/knowledge/sonar-gate.yaml +85 -0
- package/runtime/modules/kernel/failure-codes.yaml +1480 -16
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +316 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +89 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/architecture.mjs +41 -0
- package/runtime/scripts/checks/common.mjs +37 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/artifact-hold.mjs +89 -0
- package/runtime/scripts/lib/artifact-store.mjs +103 -0
- package/runtime/scripts/lib/fs-kind.mjs +10 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/hfs-allows.mjs +57 -0
- package/runtime/scripts/lib/hfs-check.mjs +254 -28
- package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
- package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
- package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
- package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
- package/runtime/scripts/lib/hfs-slots.mjs +95 -41
- package/runtime/scripts/lib/hfs-tree.mjs +80 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/redact.mjs +148 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/safe-remove.mjs +179 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/sleep-sync.mjs +17 -0
- package/runtime/scripts/lib/stack-declaration.mjs +52 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/scaffold/service.mjs +333 -0
- package/sync/format.mjs +46 -0
- package/sync/hygiene.mjs +56 -24
- package/sync/index.mjs +126 -41
- package/sync/managed.mjs +170 -0
- package/sync/skeleton.mjs +32 -10
- package/sync/sonar-key.mjs +13 -0
- package/sync/ts-strict.mjs +48 -0
- package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
- package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
- package/templates/be/hooks/husky/pre-commit +13 -0
- package/templates/be/hooks/husky/pre-push +7 -0
- package/templates/be/package-scripts/package.json +21 -0
- package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
- package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
- package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
- package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
- package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
- package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
- package/templates/be/tool-config/eslint.config.mjs +3 -0
- package/templates/be/tool-config/jest.config.js +1 -0
- package/templates/be/tool-config/prettierignore +8 -0
- package/templates/be/tool-config/prettierrc +1 -0
- package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
- package/templates/be/tool-config/tsconfig.build.json +5 -0
- package/templates/be/tool-config/tsconfig.json +11 -0
- package/templates/common/gitignore.base +1 -1
- package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
- package/templates/fe/hooks/husky/pre-commit +16 -0
- package/templates/fe/hooks/husky/pre-push +6 -0
- package/templates/fe/package-scripts/package.json +17 -0
- package/templates/fe/parts/api-client.ts +44 -0
- package/templates/fe/parts/api-outcome.ts +7 -0
- package/templates/fe/quality-config/sonar-project.properties +8 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
- package/templates/fe/tool-config/eslint.config.mjs +3 -0
- package/templates/fe/tool-config/prettierignore +10 -0
- package/templates/fe/tool-config/prettierrc +1 -0
- package/templates/fe/tool-config/stylelint.config.mjs +3 -0
- package/templates/fe/tool-config/tsconfig.json +4 -0
- package/templates/be/pre-commit +0 -8
- package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
- package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
- package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
- package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
- package/templates/common/codecov.yml +0 -13
- package/templates/common/pre-push +0 -5
- package/templates/fe/e2e.yml +0 -22
- package/templates/fe/pre-commit +0 -7
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
- package/templates/fe/sonar-project.properties +0 -11
- /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
- /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isInside } from './config.mjs';
|
|
3
|
+
import { isUnshadowedCommonJsRequire, referencedExports, relativePath, sourceLocation, UNPROVEN_FRAMEWORK, unwrapExpression } from './typescript.mjs';
|
|
4
|
+
|
|
5
|
+
export const PUBLIC_CONTRACT_RULE_ID = 'BE_PUBLIC_CONTRACT_FORM';
|
|
6
|
+
export const READONLY_BOUNDARY_RULE_ID = 'BE_READONLY_BOUNDARY';
|
|
7
|
+
|
|
8
|
+
const FRAMEWORK_EXPORTS = new Map([
|
|
9
|
+
['@nestjs/common', new Set(['Controller', 'Inject', 'Injectable', 'Module'])],
|
|
10
|
+
['@nestjs/cqrs', new Set(['CommandHandler', 'QueryHandler'])],
|
|
11
|
+
['@nestjs/graphql', new Set(['Resolver'])],
|
|
12
|
+
]);
|
|
13
|
+
const NEST_CREATED = new Set(['CommandHandler', 'Controller', 'Injectable', 'QueryHandler', 'Resolver']);
|
|
14
|
+
const MESSAGE_HANDLERS = new Set(['CommandHandler', 'QueryHandler']);
|
|
15
|
+
const NON_API_SOURCE_ROLES = new Set(['config', 'configuration', 'constants', 'main', 'module', 'module-definition', 'providers']);
|
|
16
|
+
const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
|
|
17
|
+
|
|
18
|
+
function canonical(file) {
|
|
19
|
+
return path.resolve(file);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizedSymbolValue(ts, checker, value) {
|
|
23
|
+
let symbol = value ?? null;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
26
|
+
seen.add(symbol);
|
|
27
|
+
const target = checker.getAliasedSymbol(symbol);
|
|
28
|
+
if (!target || target === symbol) break;
|
|
29
|
+
symbol = target;
|
|
30
|
+
}
|
|
31
|
+
return symbol;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedSymbol(ts, checker, node) {
|
|
35
|
+
return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function selectedNode(ts, expression) {
|
|
39
|
+
expression = unwrapExpression(ts, expression);
|
|
40
|
+
if (ts.isIdentifier(expression)) return expression;
|
|
41
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name;
|
|
42
|
+
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function valueSymbol(ts, checker, node, seen = new Set()) {
|
|
47
|
+
const selected = selectedNode(ts, node) ?? node;
|
|
48
|
+
const symbol = normalizedSymbol(ts, checker, selected);
|
|
49
|
+
if (!symbol || seen.has(symbol)) return symbol;
|
|
50
|
+
seen.add(symbol);
|
|
51
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
52
|
+
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
53
|
+
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
54
|
+
return valueSymbol(ts, checker, declarations[0].initializer, seen);
|
|
55
|
+
}
|
|
56
|
+
return symbol;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function frameworkKindForSymbol(symbol, targets) {
|
|
60
|
+
const selected = targets.get(symbol);
|
|
61
|
+
if (selected) return selected;
|
|
62
|
+
const name = symbol?.getName?.();
|
|
63
|
+
if (!name) return null;
|
|
64
|
+
for (const [packageName, exports] of FRAMEWORK_EXPORTS) {
|
|
65
|
+
if (!exports.has(name)) continue;
|
|
66
|
+
const marker = `/node_modules/${packageName}/`;
|
|
67
|
+
if ((symbol.getDeclarations?.() ?? []).some(declaration => declaration.getSourceFile().fileName.replaceAll('\\', '/').includes(marker))) return name;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function returnedExpressions(ts, declaration) {
|
|
73
|
+
if ((ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) && !ts.isBlock(declaration.body)) return [declaration.body];
|
|
74
|
+
const body = ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)
|
|
75
|
+
|| ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration) ? declaration.body : null;
|
|
76
|
+
if (!body || !ts.isBlock(body)) return [];
|
|
77
|
+
const returned = [];
|
|
78
|
+
const visit = node => {
|
|
79
|
+
if (node !== body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
80
|
+
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
|
|
81
|
+
if (ts.isReturnStatement(node) && node.expression) returned.push(node.expression);
|
|
82
|
+
else ts.forEachChild(node, visit);
|
|
83
|
+
};
|
|
84
|
+
visit(body);
|
|
85
|
+
return returned;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tracedFrameworkKinds(ts, checker, expression, targets, seen = new Set(), depth = 0) {
|
|
89
|
+
if (!expression) return new Set();
|
|
90
|
+
if (depth > 10) return new Set([UNPROVEN_FRAMEWORK]);
|
|
91
|
+
expression = unwrapExpression(ts, expression);
|
|
92
|
+
const selected = selectedNode(ts, expression);
|
|
93
|
+
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
94
|
+
const direct = symbol ? frameworkKindForSymbol(valueSymbol(ts, checker, selected), targets) : null;
|
|
95
|
+
if (direct) return new Set([direct]);
|
|
96
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
97
|
+
const kinds = new Set();
|
|
98
|
+
for (const returned of returnedExpressions(ts, expression)) {
|
|
99
|
+
for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, seen, depth + 1)) kinds.add(kind);
|
|
100
|
+
}
|
|
101
|
+
return kinds;
|
|
102
|
+
}
|
|
103
|
+
if (ts.isCallExpression(expression)) {
|
|
104
|
+
const kinds = tracedFrameworkKinds(ts, checker, expression.expression, targets, seen, depth + 1);
|
|
105
|
+
if (kinds.size) return kinds;
|
|
106
|
+
}
|
|
107
|
+
if (!symbol || seen.has(symbol)) return new Set();
|
|
108
|
+
const nextSeen = new Set(seen).add(symbol);
|
|
109
|
+
const kinds = new Set();
|
|
110
|
+
for (const declaration of symbol.getDeclarations?.() ?? []) {
|
|
111
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
112
|
+
for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, nextSeen, depth + 1)) kinds.add(kind);
|
|
113
|
+
}
|
|
114
|
+
if (ts.isShorthandPropertyAssignment(declaration)) {
|
|
115
|
+
const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
|
|
116
|
+
const kind = frameworkKindForSymbol(target, targets);
|
|
117
|
+
if (kind) kinds.add(kind);
|
|
118
|
+
}
|
|
119
|
+
if (ts.isPropertyAssignment(declaration)) {
|
|
120
|
+
for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, nextSeen, depth + 1)) kinds.add(kind);
|
|
121
|
+
}
|
|
122
|
+
for (const returned of returnedExpressions(ts, declaration)) {
|
|
123
|
+
for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, nextSeen, depth + 1)) kinds.add(kind);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return kinds;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function frameworkTargets(config, context, checker, localFiles) {
|
|
130
|
+
const bySymbol = new Map();
|
|
131
|
+
const reasons = [];
|
|
132
|
+
const program = context.programs.find(candidate => candidate.getTypeChecker() === checker);
|
|
133
|
+
if (!program) return { bySymbol, reasons: ['a TypeScript program checker could not be associated with its source'] };
|
|
134
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
135
|
+
if (!localFiles.has(canonical(sourceFile.fileName))
|
|
136
|
+
&& !(sourceFile.isDeclarationFile && isInside(config.root, sourceFile.fileName)
|
|
137
|
+
&& !sourceFile.fileName.replaceAll('\\', '/').includes('/node_modules/'))) continue;
|
|
138
|
+
for (const statement of sourceFile.statements) {
|
|
139
|
+
const moduleSpecifier = (context.ts.isImportDeclaration(statement) || context.ts.isExportDeclaration(statement))
|
|
140
|
+
&& statement.moduleSpecifier && context.ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier : null;
|
|
141
|
+
const importEquals = context.ts.isImportEqualsDeclaration(statement) && context.ts.isExternalModuleReference(statement.moduleReference)
|
|
142
|
+
&& statement.moduleReference.expression && context.ts.isStringLiteralLike(statement.moduleReference.expression)
|
|
143
|
+
? statement.moduleReference.expression : null;
|
|
144
|
+
const specifierNode = moduleSpecifier ?? importEquals;
|
|
145
|
+
const specifier = specifierNode?.text;
|
|
146
|
+
if (!specifier || !FRAMEWORK_EXPORTS.has(specifier)) continue;
|
|
147
|
+
const moduleSymbol = checker.getSymbolAtLocation(specifierNode);
|
|
148
|
+
if (!moduleSymbol) {
|
|
149
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${specifier}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const selected = FRAMEWORK_EXPORTS.get(specifier);
|
|
153
|
+
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
|
|
154
|
+
for (const name of referencedExports(context.ts, statement, selected)) {
|
|
155
|
+
const target = exports.get(name);
|
|
156
|
+
if (target) bySymbol.set(target, name);
|
|
157
|
+
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${specifier}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const visitRequire = node => {
|
|
161
|
+
if (context.ts.isCallExpression(node) && isUnshadowedCommonJsRequire(context.ts, checker, node.expression)
|
|
162
|
+
&& node.arguments.length === 1 && context.ts.isStringLiteralLike(node.arguments[0])
|
|
163
|
+
&& FRAMEWORK_EXPORTS.has(node.arguments[0].text)) {
|
|
164
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a CommonJS ${node.arguments[0].text} binding whose contract role cannot be proved`);
|
|
165
|
+
}
|
|
166
|
+
context.ts.forEachChild(node, visitRequire);
|
|
167
|
+
};
|
|
168
|
+
visitRequire(sourceFile);
|
|
169
|
+
}
|
|
170
|
+
return { bySymbol, reasons };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function calledExpression(ts, decorator) {
|
|
174
|
+
return ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function decoratorKind(ts, checker, decorator, targets) {
|
|
178
|
+
const selected = selectedNode(ts, calledExpression(ts, decorator));
|
|
179
|
+
return selected ? frameworkKindForSymbol(valueSymbol(ts, checker, selected), targets) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function constructedDecoratorKind(ts, checker, decorator, targets) {
|
|
183
|
+
const kinds = tracedFrameworkKinds(ts, checker, calledExpression(ts, decorator), targets);
|
|
184
|
+
if (kinds.has(UNPROVEN_FRAMEWORK)) return 'unproven framework';
|
|
185
|
+
return kinds.size === 1 ? [...kinds][0] : kinds.size ? 'multiple framework' : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function decorators(ts, node) {
|
|
189
|
+
return ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : node.decorators ?? [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function modifiers(ts, node) {
|
|
193
|
+
return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function hasModifier(ts, node, kind) {
|
|
197
|
+
return modifiers(ts, node).some(modifier => modifier.kind === kind);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isPublicMember(ts, node) {
|
|
201
|
+
return !hasModifier(ts, node, ts.SyntaxKind.PrivateKeyword) && !hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readonlyMember(ts, node) {
|
|
205
|
+
return hasModifier(ts, node, ts.SyntaxKind.ReadonlyKeyword);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function violation(config, sourceFile, node, ruleId, message, extra = {}) {
|
|
209
|
+
return { ruleId, path: relativePath(config.root, sourceFile.fileName), ...sourceLocation(sourceFile, node), message, ...extra };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sourceRole(sourceFile) {
|
|
213
|
+
const normalized = sourceFile.fileName.replaceAll('\\', '/');
|
|
214
|
+
const base = path.basename(sourceFile.fileName).replace(SOURCE_EXTENSION, '').toLowerCase();
|
|
215
|
+
const segments = base.split('.');
|
|
216
|
+
const role = segments.length > 1 ? segments.at(-1) : base === 'use-case' || base.endsWith('-use-case') ? 'use-case' : null;
|
|
217
|
+
return { base, role, transport: normalized.includes('/transport/') };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isFrameworkHelper(sourceFile, declaration, framework) {
|
|
221
|
+
const role = sourceRole(sourceFile);
|
|
222
|
+
if (role.transport || NON_API_SOURCE_ROLES.has(role.role) || NON_API_SOURCE_ROLES.has(role.base)) return true;
|
|
223
|
+
if (declaration && declaration.name && /Module$/.test(declaration.name.text ?? '')) {
|
|
224
|
+
return decorators(framework.ts, declaration).some(decorator => decoratorKind(framework.ts, framework.checker, decorator, framework.targets) === 'Module');
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function builtinSymbol(ts, symbol, name) {
|
|
230
|
+
if (!symbol || symbol.getName?.() !== name) return false;
|
|
231
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
232
|
+
return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile
|
|
233
|
+
&& /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function contractTypeStatus(ts, checker, node, seen = new Set(), depth = 0) {
|
|
237
|
+
if (!node || depth > 12) return 'unavailable';
|
|
238
|
+
if (ts.isParenthesizedTypeNode(node)) return contractTypeStatus(ts, checker, node.type, seen, depth + 1);
|
|
239
|
+
if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
|
|
240
|
+
return contractTypeStatus(ts, checker, node.type, seen, depth + 1);
|
|
241
|
+
}
|
|
242
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
243
|
+
const symbol = normalizedSymbol(ts, checker, node.typeName);
|
|
244
|
+
if (!symbol) return 'unavailable';
|
|
245
|
+
if (builtinSymbol(ts, symbol, 'Promise') || builtinSymbol(ts, symbol, 'Readonly') || builtinSymbol(ts, symbol, 'Awaited')) {
|
|
246
|
+
return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
|
|
247
|
+
}
|
|
248
|
+
if (builtinSymbol(ts, symbol, 'Array') || builtinSymbol(ts, symbol, 'ReadonlyArray')) {
|
|
249
|
+
return node.typeArguments?.length === 1 ? contractTypeStatus(ts, checker, node.typeArguments[0], seen, depth + 1) : 'unavailable';
|
|
250
|
+
}
|
|
251
|
+
if (builtinSymbol(ts, symbol, 'Record') || builtinSymbol(ts, symbol, 'Partial') || builtinSymbol(ts, symbol, 'Pick')
|
|
252
|
+
|| builtinSymbol(ts, symbol, 'Omit') || builtinSymbol(ts, symbol, 'Required')) return 'inline';
|
|
253
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
254
|
+
if (!declarations.length) return 'unavailable';
|
|
255
|
+
return declarations.every(declaration => ts.isTypeParameterDeclaration(declaration)
|
|
256
|
+
|| ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration)
|
|
257
|
+
|| ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) ? 'named' : 'unavailable';
|
|
258
|
+
}
|
|
259
|
+
if (ts.isArrayTypeNode(node)) return contractTypeStatus(ts, checker, node.elementType, seen, depth + 1);
|
|
260
|
+
if (ts.isTypeLiteralNode(node) || ts.isMappedTypeNode(node) || ts.isUnionTypeNode(node)
|
|
261
|
+
|| ts.isIntersectionTypeNode(node) || ts.isTupleTypeNode(node) || ts.isFunctionTypeNode(node)) return 'inline';
|
|
262
|
+
if (ts.isConditionalTypeNode(node) || ts.isIndexedAccessTypeNode(node) || ts.isInferTypeNode(node)
|
|
263
|
+
|| ts.isTypeQueryNode(node) || ts.isImportTypeNode(node)) return 'unavailable';
|
|
264
|
+
return 'scalar';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function contractTypeStatusFromType(ts, checker, type, seen = new Set(), depth = 0) {
|
|
268
|
+
if (!type || depth > 12 || seen.has(type)) return 'unavailable';
|
|
269
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'unavailable';
|
|
270
|
+
const nextSeen = new Set(seen).add(type);
|
|
271
|
+
const named = type.aliasSymbol ?? type.symbol ?? null;
|
|
272
|
+
for (const wrapper of ['Promise', 'Readonly', 'Awaited', 'Array', 'ReadonlyArray']) if (builtinSymbol(ts, named, wrapper)) {
|
|
273
|
+
const argumentsList = type.aliasTypeArguments ?? (checker.getTypeArguments && (type.objectFlags & ts.ObjectFlags.Reference)
|
|
274
|
+
? checker.getTypeArguments(type) : type.typeArguments) ?? [];
|
|
275
|
+
return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
|
|
276
|
+
}
|
|
277
|
+
for (const anonymous of ['Record', 'Partial', 'Pick', 'Omit', 'Required']) if (builtinSymbol(ts, named, anonymous)) return 'inline';
|
|
278
|
+
if (type.aliasSymbol) return 'named';
|
|
279
|
+
if (type.symbol?.getDeclarations?.().some(declaration => ts.isEnumDeclaration(declaration))) return 'named';
|
|
280
|
+
if (type.flags & ts.TypeFlags.TypeParameter) return 'named';
|
|
281
|
+
if (type.flags & ts.TypeFlags.Boolean) return 'scalar';
|
|
282
|
+
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) return 'inline';
|
|
283
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
284
|
+
if (checker.isTupleType?.(type)) return 'inline';
|
|
285
|
+
if (checker.isArrayType?.(type)) {
|
|
286
|
+
const argumentsList = checker.getTypeArguments?.(type) ?? [];
|
|
287
|
+
return argumentsList.length === 1 ? contractTypeStatusFromType(ts, checker, argumentsList[0], nextSeen, depth + 1) : 'unavailable';
|
|
288
|
+
}
|
|
289
|
+
const declarations = type.symbol?.getDeclarations?.() ?? [];
|
|
290
|
+
if (type.symbol && !type.symbol.getName().startsWith('__') && declarations.some(declaration => ts.isInterfaceDeclaration(declaration)
|
|
291
|
+
|| ts.isClassDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration))) return 'named';
|
|
292
|
+
return 'inline';
|
|
293
|
+
}
|
|
294
|
+
if (type.flags & (ts.TypeFlags.Conditional | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Substitution)) return 'unavailable';
|
|
295
|
+
return 'scalar';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function callableSignatures(ts, symbol, checker, location) {
|
|
299
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, location);
|
|
300
|
+
const signatures = checker.getSignaturesOfType(type, ts.SignatureKind.Call);
|
|
301
|
+
const bodyless = signatures.filter(signature => signature.getDeclaration() && !signature.getDeclaration().body);
|
|
302
|
+
return bodyless.length ? bodyless : signatures.filter(signature => signature.getDeclaration());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function checkSignature(config, context, checker, signature, location, reportSource, reportNode, displayName, violations, reasons) {
|
|
306
|
+
const ts = context.ts;
|
|
307
|
+
const declaration = signature.getDeclaration();
|
|
308
|
+
let signatureIndex = 0;
|
|
309
|
+
for (let index = 0; index < (declaration.parameters?.length ?? 0); index += 1) {
|
|
310
|
+
const parameter = declaration.parameters[index];
|
|
311
|
+
if (parameter.name && ts.isIdentifier(parameter.name) && parameter.name.text === 'this') continue;
|
|
312
|
+
if (!parameter.type) {
|
|
313
|
+
violations.push(violation(config, reportSource, reportNode ?? parameter, PUBLIC_CONTRACT_RULE_ID,
|
|
314
|
+
`${displayName} must declare the type of each public input.`));
|
|
315
|
+
signatureIndex += 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const parameterSymbol = signature.getParameters()[signatureIndex];
|
|
319
|
+
signatureIndex += 1;
|
|
320
|
+
const actual = parameterSymbol ? checker.getTypeOfSymbolAtLocation(parameterSymbol, location) : null;
|
|
321
|
+
let status;
|
|
322
|
+
if (actual && parameter.questionToken && !ts.isUnionTypeNode(parameter.type) && (actual.flags & ts.TypeFlags.Union)) {
|
|
323
|
+
const selected = actual.types.filter(type => !(type.flags & ts.TypeFlags.Undefined));
|
|
324
|
+
status = selected.length === 1 ? contractTypeStatusFromType(ts, checker, selected[0]) : 'inline';
|
|
325
|
+
} else status = actual ? contractTypeStatusFromType(ts, checker, actual) : contractTypeStatus(ts, checker, parameter.type);
|
|
326
|
+
if (status === 'inline') violations.push(violation(config, reportSource, reportNode ?? parameter.type, PUBLIC_CONTRACT_RULE_ID,
|
|
327
|
+
`${displayName} uses an inline object/union input; expose a named contract or a positional primitive.`));
|
|
328
|
+
else if (status === 'unavailable') reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot prove the public input type of ${displayName}`);
|
|
329
|
+
}
|
|
330
|
+
if (!declaration.type) {
|
|
331
|
+
violations.push(violation(config, reportSource, reportNode ?? declaration, PUBLIC_CONTRACT_RULE_ID,
|
|
332
|
+
`${displayName} must declare an explicit public output type.`));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const actual = checker.getReturnTypeOfSignature(signature);
|
|
336
|
+
const status = actual ? contractTypeStatusFromType(ts, checker, actual) : contractTypeStatus(ts, checker, declaration.type);
|
|
337
|
+
if (status === 'inline') violations.push(violation(config, reportSource, reportNode ?? declaration.type, PUBLIC_CONTRACT_RULE_ID,
|
|
338
|
+
`${displayName} uses an inline object/union output; expose a named result contract or a primitive.`));
|
|
339
|
+
else if (status === 'unavailable') reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot prove the public output type of ${displayName}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function symbolName(symbol) {
|
|
343
|
+
const name = symbol.getName?.() ?? '(anonymous)';
|
|
344
|
+
return name === '__call' ? '(callable)' : name;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isRepositoryDeclaration(declaration, localFiles) {
|
|
348
|
+
return localFiles.has(canonical(declaration.getSourceFile().fileName));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function callableMembers(ts, checker, symbol, location, localFiles) {
|
|
352
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
353
|
+
const classExpressions = declarations.flatMap(declaration => ts.isVariableDeclaration(declaration)
|
|
354
|
+
&& declaration.initializer && ts.isClassExpression(unwrapExpression(ts, declaration.initializer))
|
|
355
|
+
? [unwrapExpression(ts, declaration.initializer)] : []);
|
|
356
|
+
const valueType = checker.getTypeOfSymbolAtLocation(symbol, location);
|
|
357
|
+
const declaredClassLike = declarations.some(declaration => ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration));
|
|
358
|
+
const primary = declaredClassLike ? checker.getDeclaredTypeOfSymbol(symbol) : valueType;
|
|
359
|
+
const types = [primary];
|
|
360
|
+
if (declaredClassLike || classExpressions.length) types.push(valueType);
|
|
361
|
+
for (const signature of checker.getSignaturesOfType(valueType, ts.SignatureKind.Construct)) types.push(checker.getReturnTypeOfSignature(signature));
|
|
362
|
+
const direct = checker.getSignaturesOfType(primary, ts.SignatureKind.Call).length ? [symbol] : [];
|
|
363
|
+
const members = types.flatMap(type => checker.getPropertiesOfType(type)).filter(member => {
|
|
364
|
+
if (member.getName() === 'prototype') return false;
|
|
365
|
+
const selected = member.getDeclarations?.() ?? [];
|
|
366
|
+
return selected.some(declaration => (ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)
|
|
367
|
+
|| ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration) || ts.isGetAccessorDeclaration(declaration))
|
|
368
|
+
&& isPublicMember(ts, declaration) && isRepositoryDeclaration(declaration, localFiles))
|
|
369
|
+
&& checker.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(member, location), ts.SignatureKind.Call).length;
|
|
370
|
+
});
|
|
371
|
+
return [...new Set([...direct, ...members])];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function exportedSymbols(ts, checker, sourceFile) {
|
|
375
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
376
|
+
return moduleSymbol ? checker.getExportsOfModule(moduleSymbol).map(symbol => normalizedSymbolValue(ts, checker, symbol)).filter(Boolean) : [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function checkCallableSymbol(config, context, checker, symbol, location, reportSource, reportNode, violations, reasons, seen, localFiles) {
|
|
380
|
+
if (seen.has(symbol)) return 0;
|
|
381
|
+
seen.add(symbol);
|
|
382
|
+
let checked = 0;
|
|
383
|
+
for (const callable of callableMembers(context.ts, checker, symbol, location, localFiles)) {
|
|
384
|
+
if (callable !== symbol && seen.has(callable)) continue;
|
|
385
|
+
seen.add(callable);
|
|
386
|
+
const signatures = callableSignatures(context.ts, callable, checker, location);
|
|
387
|
+
if (!signatures.length) {
|
|
388
|
+
reasons.push(`${relativePath(config.root, reportSource.fileName)} cannot resolve the callable signature of ${symbolName(callable)}`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
for (const signature of signatures) {
|
|
392
|
+
checked += 1;
|
|
393
|
+
checkSignature(config, context, checker, signature, location, reportSource, reportNode,
|
|
394
|
+
callable === symbol ? symbolName(symbol) : `${symbolName(symbol)}.${symbolName(callable)}`,
|
|
395
|
+
violations, reasons);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return checked;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function propertyName(ts, node) {
|
|
402
|
+
const name = node.name;
|
|
403
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isPrivateIdentifier(name)) return name.text;
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function thisOriginStatus(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
408
|
+
if (!expression || depth > 8) return 'no';
|
|
409
|
+
expression = unwrapExpression(ts, expression);
|
|
410
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword) return 'yes';
|
|
411
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
412
|
+
if (!symbol || seen.has(symbol)) return 'no';
|
|
413
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
414
|
+
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer) return 'no';
|
|
415
|
+
const origin = thisOriginStatus(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
|
|
416
|
+
if (origin === 'no') return 'no';
|
|
417
|
+
return (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const) ? origin : 'unavailable';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function instanceAssignmentTarget(ts, checker, expression) {
|
|
421
|
+
expression = unwrapExpression(ts, expression);
|
|
422
|
+
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return { instance: false };
|
|
423
|
+
const owner = thisOriginStatus(ts, checker, expression.expression);
|
|
424
|
+
if (owner === 'no') return { instance: false };
|
|
425
|
+
if (owner === 'unavailable') return { instance: true, name: null, supported: false };
|
|
426
|
+
if (ts.isPropertyAccessExpression(expression)) return { instance: true, name: expression.name.text, supported: true };
|
|
427
|
+
if (ts.isStringLiteralLike(expression.argumentExpression)) return { instance: true, name: expression.argumentExpression.text, supported: true };
|
|
428
|
+
return { instance: true, name: null, supported: false };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function expressionOrigin(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
432
|
+
if (!expression || depth > 8) return null;
|
|
433
|
+
expression = unwrapExpression(ts, expression);
|
|
434
|
+
const shorthand = ts.isShorthandPropertyAssignment(expression) ? checker.getShorthandAssignmentValueSymbol?.(expression) : null;
|
|
435
|
+
const symbol = normalizedSymbolValue(ts, checker, shorthand) ?? normalizedSymbol(ts, checker, expression);
|
|
436
|
+
if (!symbol || seen.has(symbol)) return symbol;
|
|
437
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
438
|
+
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
439
|
+
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
440
|
+
return expressionOrigin(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
|
|
441
|
+
}
|
|
442
|
+
return symbol;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function expressionReferencesOrigin(ts, checker, expression, expected) {
|
|
446
|
+
let found = false;
|
|
447
|
+
const visit = node => {
|
|
448
|
+
if (found) return;
|
|
449
|
+
if (expressionOrigin(ts, checker, node) === expected) {
|
|
450
|
+
found = true;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
ts.forEachChild(node, visit);
|
|
454
|
+
};
|
|
455
|
+
visit(expression);
|
|
456
|
+
return found;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function isSafePrimitiveProjection(ts, checker, expression, expected) {
|
|
460
|
+
expression = unwrapExpression(ts, expression);
|
|
461
|
+
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false;
|
|
462
|
+
if (expressionOrigin(ts, checker, expression.expression) !== expected) return false;
|
|
463
|
+
const type = checker.getTypeAtLocation(expression);
|
|
464
|
+
const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike
|
|
465
|
+
| ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
466
|
+
| ts.TypeFlags.Void | ts.TypeFlags.Never;
|
|
467
|
+
return Boolean(type.flags & primitive) && !(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function assignmentPatternHasInstanceTarget(ts, checker, expression) {
|
|
471
|
+
let found = false;
|
|
472
|
+
const visit = node => {
|
|
473
|
+
if (found) return;
|
|
474
|
+
const target = instanceAssignmentTarget(ts, checker, node);
|
|
475
|
+
if (target.instance) {
|
|
476
|
+
found = true;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
ts.forEachChild(node, visit);
|
|
480
|
+
};
|
|
481
|
+
visit(expression);
|
|
482
|
+
return found;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function globalLibraryIdentifier(ts, checker, node, expected) {
|
|
486
|
+
if (!ts.isIdentifier(node) || node.text !== expected) return false;
|
|
487
|
+
const symbol = normalizedSymbol(ts, checker, node);
|
|
488
|
+
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
489
|
+
return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile
|
|
490
|
+
&& /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function instanceMutationCall(ts, checker, node) {
|
|
494
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
|
|
495
|
+
const owner = node.expression.expression;
|
|
496
|
+
const method = node.expression.name.text;
|
|
497
|
+
const known = (method === 'assign' || method === 'defineProperty') && globalLibraryIdentifier(ts, checker, owner, 'Object')
|
|
498
|
+
|| method === 'set' && globalLibraryIdentifier(ts, checker, owner, 'Reflect');
|
|
499
|
+
if (!known || thisOriginStatus(ts, checker, node.arguments[0]) === 'no') return null;
|
|
500
|
+
return { node, values: node.arguments.slice(method === 'assign' ? 1 : 2) };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function constructorAssignments(ts, checker, constructor, parameter) {
|
|
504
|
+
if (!constructor.body) return [];
|
|
505
|
+
const parameterSymbol = normalizedSymbol(ts, checker, parameter.name);
|
|
506
|
+
const assignments = [];
|
|
507
|
+
const visit = node => {
|
|
508
|
+
if (node !== constructor.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
509
|
+
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
|
|
510
|
+
if (ts.isBinaryExpression(node) && expressionReferencesOrigin(ts, checker, node.right, parameterSymbol)
|
|
511
|
+
&& !isSafePrimitiveProjection(ts, checker, node.right, parameterSymbol)) {
|
|
512
|
+
const target = instanceAssignmentTarget(ts, checker, node.left);
|
|
513
|
+
if (target.instance) assignments.push({ node, name: target.name,
|
|
514
|
+
supported: target.supported && node.operatorToken.kind === ts.SyntaxKind.EqualsToken });
|
|
515
|
+
else if (assignmentPatternHasInstanceTarget(ts, checker, node.left)) assignments.push({ node, name: null, supported: false });
|
|
516
|
+
else ts.forEachChild(node, visit);
|
|
517
|
+
} else {
|
|
518
|
+
const mutation = instanceMutationCall(ts, checker, node);
|
|
519
|
+
if (mutation?.values.some(argument => expressionReferencesOrigin(ts, checker, argument, parameterSymbol))) {
|
|
520
|
+
assignments.push({ node, name: null, supported: false });
|
|
521
|
+
} else ts.forEachChild(node, visit);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
visit(constructor.body);
|
|
525
|
+
return assignments;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function checkReadonlyProperty(config, context, sourceFile, node, label, violations) {
|
|
529
|
+
if (!readonlyMember(context.ts, node)) violations.push(violation(config, sourceFile, node.name ?? node, READONLY_BOUNDARY_RULE_ID,
|
|
530
|
+
`${label} must be readonly; execution state must not mutate an injected dependency or received message.`));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function classValuesForSymbol(ts, symbol, localFiles) {
|
|
534
|
+
return (symbol?.getDeclarations?.() ?? []).flatMap(declaration => {
|
|
535
|
+
if (ts.isClassDeclaration(declaration)) return [declaration];
|
|
536
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
537
|
+
const initializer = unwrapExpression(ts, declaration.initializer);
|
|
538
|
+
if (ts.isClassExpression(initializer)) return [initializer];
|
|
539
|
+
}
|
|
540
|
+
return [];
|
|
541
|
+
}).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function messageClass(config, context, checker, decorator, localFiles, reasons) {
|
|
545
|
+
const expression = decorator.expression;
|
|
546
|
+
if (!context.ts.isCallExpression(expression) || expression.arguments.length < 1) {
|
|
547
|
+
reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} has a handler decorator without a static message class`);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
const symbol = valueSymbol(context.ts, checker, expression.arguments[0]);
|
|
551
|
+
const declarations = classValuesForSymbol(context.ts, symbol, localFiles);
|
|
552
|
+
if (declarations.length !== 1) {
|
|
553
|
+
reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} cannot resolve one local message class from its handler decorator`);
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return declarations[0];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function checkMessageReadonly(config, context, checker, declaration, localFiles, violations, reasons) {
|
|
560
|
+
const ts = context.ts;
|
|
561
|
+
const visited = new Set();
|
|
562
|
+
const visitClass = selected => {
|
|
563
|
+
const key = `${canonical(selected.getSourceFile().fileName)}:${selected.pos}`;
|
|
564
|
+
if (visited.has(key)) return;
|
|
565
|
+
visited.add(key);
|
|
566
|
+
const sourceFile = selected.getSourceFile();
|
|
567
|
+
for (const member of selected.members) {
|
|
568
|
+
if (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword)) {
|
|
569
|
+
checkReadonlyProperty(config, context, sourceFile, member, `Message field ${propertyName(ts, member) ?? '(computed)'}`, violations);
|
|
570
|
+
}
|
|
571
|
+
if (ts.isSetAccessorDeclaration(member)) violations.push(violation(config, sourceFile, member.name, READONLY_BOUNDARY_RULE_ID,
|
|
572
|
+
`Message field ${propertyName(ts, member) ?? '(computed)'} cannot expose a setter.`));
|
|
573
|
+
if (ts.isIndexSignatureDeclaration(member)) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has a message index signature whose mutation boundary cannot be proved`);
|
|
574
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
575
|
+
for (const parameter of member.parameters) if (modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword,
|
|
576
|
+
ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind))) {
|
|
577
|
+
checkReadonlyProperty(config, context, sourceFile, parameter, `Message field ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
|
|
578
|
+
}
|
|
579
|
+
const inspectAssign = node => {
|
|
580
|
+
if (node !== member.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
|
|
581
|
+
if (instanceMutationCall(ts, checker, node)) {
|
|
582
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance mutation`);
|
|
583
|
+
} else if (ts.isBinaryExpression(node) && assignmentPatternHasInstanceTarget(ts, checker, node.left)
|
|
584
|
+
&& !instanceAssignmentTarget(ts, checker, node.left).supported) {
|
|
585
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance assignment`);
|
|
586
|
+
} else ts.forEachChild(node, inspectAssign);
|
|
587
|
+
};
|
|
588
|
+
if (member.body) inspectAssign(member.body);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const type = classInstanceType(ts, checker, selected);
|
|
592
|
+
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
593
|
+
const declarations = base.symbol?.getDeclarations?.().filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
594
|
+
if (declarations.length !== 1) {
|
|
595
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
|
|
596
|
+
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) visitClass(declarations[0]);
|
|
597
|
+
else if (classCarriesState(ts, declarations[0])) {
|
|
598
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message state from a class outside the checked production program`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
visitClass(declaration);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function classCarriesState(ts, declaration) {
|
|
606
|
+
return declaration.members.some(member => (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword))
|
|
607
|
+
|| ts.isSetAccessorDeclaration(member) || ts.isIndexSignatureDeclaration(member)
|
|
608
|
+
|| (ts.isConstructorDeclaration(member) && member.parameters.length > 0));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function classInstanceType(ts, checker, declaration) {
|
|
612
|
+
const symbol = declaration.name ? normalizedSymbol(ts, checker, declaration.name) : null;
|
|
613
|
+
if (symbol && ts.isClassDeclaration(declaration)) return checker.getDeclaredTypeOfSymbol(symbol);
|
|
614
|
+
const value = checker.getTypeAtLocation(declaration);
|
|
615
|
+
const constructor = checker.getSignaturesOfType(value, ts.SignatureKind.Construct)[0];
|
|
616
|
+
return constructor ? checker.getReturnTypeOfSignature(constructor) : value;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function checkInjectedClass(config, context, checker, declaration, classKind, targets, localFiles, violations, reasons, visited = new Set()) {
|
|
620
|
+
const ts = context.ts;
|
|
621
|
+
const classKey = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
|
|
622
|
+
if (visited.has(classKey)) return;
|
|
623
|
+
visited.add(classKey);
|
|
624
|
+
const sourceFile = declaration.getSourceFile();
|
|
625
|
+
const properties = new Map(declaration.members.filter(member => ts.isPropertyDeclaration(member))
|
|
626
|
+
.map(member => [propertyName(ts, member), member]).filter(([name]) => name));
|
|
627
|
+
for (const member of declaration.members) {
|
|
628
|
+
if (ts.isPropertyDeclaration(member)) {
|
|
629
|
+
const injected = decorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject');
|
|
630
|
+
if (injected) checkReadonlyProperty(config, context, sourceFile, member, `Injected property ${propertyName(ts, member) ?? '(computed)'}`, violations);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (!ts.isConstructorDeclaration(member)) continue;
|
|
634
|
+
for (const parameter of member.parameters) {
|
|
635
|
+
const explicitInject = decorators(ts, parameter).some(decorator => decoratorKind(ts, checker, decorator, targets) === 'Inject');
|
|
636
|
+
if (!classKind && !explicitInject) continue;
|
|
637
|
+
const parameterProperty = modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
|
|
638
|
+
ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind));
|
|
639
|
+
if (parameterProperty) {
|
|
640
|
+
checkReadonlyProperty(config, context, sourceFile, parameter, `Injected dependency ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
for (const assignment of constructorAssignments(ts, checker, member, parameter)) {
|
|
644
|
+
if (!assignment.supported || !assignment.name) {
|
|
645
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)}:${sourceLocation(sourceFile, assignment.node).line} stores an injected dependency through an unsupported instance assignment`);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const property = properties.get(assignment.name);
|
|
649
|
+
if (!property) reasons.push(`${relativePath(config.root, sourceFile.fileName)} assigns injected dependency ${assignment.name} without a declared field`);
|
|
650
|
+
else checkReadonlyProperty(config, context, sourceFile, property, `Injected dependency ${assignment.name}`, violations);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (classKind) {
|
|
655
|
+
const type = classInstanceType(ts, checker, declaration);
|
|
656
|
+
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
657
|
+
const declarations = base.symbol?.getDeclarations?.().filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
658
|
+
if (declarations.length !== 1) {
|
|
659
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
660
|
+
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
|
|
661
|
+
checkInjectedClass(config, context, checker, declarations[0], classKind, targets, localFiles, violations, reasons, visited);
|
|
662
|
+
} else if (classCarriesState(ts, declarations[0])) {
|
|
663
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Enforce mechanically selected public contract and readonly boundaries without treating DTOs or arbitrary interfaces as APIs. */
|
|
670
|
+
export function checkBackendContracts(config, context) {
|
|
671
|
+
const ts = context.ts;
|
|
672
|
+
const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
|
|
673
|
+
const sourceFiles = new Map(context.files.map(file => [canonical(file.fileName), file]));
|
|
674
|
+
const frameworkByChecker = new Map();
|
|
675
|
+
const violations = [];
|
|
676
|
+
const publicReasons = [];
|
|
677
|
+
const readonlyReasons = [];
|
|
678
|
+
const publicSeen = new Set();
|
|
679
|
+
const messageSeen = new Set();
|
|
680
|
+
let callableSignatures = 0;
|
|
681
|
+
let injectedClasses = 0;
|
|
682
|
+
let messages = 0;
|
|
683
|
+
|
|
684
|
+
const frameworkFor = checker => {
|
|
685
|
+
if (!frameworkByChecker.has(checker)) {
|
|
686
|
+
const selected = frameworkTargets(config, context, checker, localFiles);
|
|
687
|
+
frameworkByChecker.set(checker, { ...selected, ts, checker, targets: selected.bySymbol });
|
|
688
|
+
readonlyReasons.push(...selected.reasons);
|
|
689
|
+
}
|
|
690
|
+
return frameworkByChecker.get(checker);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
if (!config.owners?.length) publicReasons.push('no slot owner with an entry file exists, so capability API discovery has no owners to list');
|
|
694
|
+
const ownerRoots = (config.owners ?? []).map(owner => path.resolve(config.root, ...owner.root.split('/')));
|
|
695
|
+
const publicSourceRoots = [...config.backend.features, ...config.backend.modules].map(root => path.resolve(config.root, ...root.split('/')));
|
|
696
|
+
for (const sourceFile of context.files) if (publicSourceRoots.some(root => isInside(root, sourceFile.fileName))
|
|
697
|
+
&& !ownerRoots.some(root => isInside(root, sourceFile.fileName))) {
|
|
698
|
+
publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} is outside every declared owner root`);
|
|
699
|
+
}
|
|
700
|
+
if (config.owners) for (const owner of config.owners) {
|
|
701
|
+
const entry = sourceFiles.get(canonical(path.resolve(config.root, ...owner.entry.split('/'))));
|
|
702
|
+
if (!entry) {
|
|
703
|
+
publicReasons.push(`${owner.entry} is outside the checked production program`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const checker = context.checkerFor(entry.fileName);
|
|
707
|
+
const framework = frameworkFor(checker);
|
|
708
|
+
if (entry.statements.some(statement => ts.isExportAssignment(statement) && statement.isExportEquals)) {
|
|
709
|
+
publicReasons.push(`${owner.entry} uses export =, so named capability API discovery is unavailable`);
|
|
710
|
+
}
|
|
711
|
+
for (const symbol of exportedSymbols(ts, checker, entry)) {
|
|
712
|
+
const declarations = symbol.getDeclarations?.().filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
|
|
713
|
+
if (!declarations.length) continue;
|
|
714
|
+
const representative = declarations[0];
|
|
715
|
+
if (isFrameworkHelper(representative.getSourceFile(), representative, framework)) continue;
|
|
716
|
+
callableSignatures += checkCallableSymbol(config, context, checker, symbol, representative, representative.getSourceFile(), representative.name ?? representative,
|
|
717
|
+
violations, publicReasons, publicSeen, localFiles);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
for (const sourceFile of context.files) {
|
|
722
|
+
const checker = context.checkerFor(sourceFile.fileName);
|
|
723
|
+
const framework = frameworkFor(checker);
|
|
724
|
+
const role = sourceRole(sourceFile);
|
|
725
|
+
const classes = [];
|
|
726
|
+
const collectClasses = node => {
|
|
727
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) classes.push(node);
|
|
728
|
+
ts.forEachChild(node, collectClasses);
|
|
729
|
+
};
|
|
730
|
+
collectClasses(sourceFile);
|
|
731
|
+
for (const statement of classes) {
|
|
732
|
+
const classDecorators = decorators(ts, statement);
|
|
733
|
+
const kindsByDecorator = classDecorators.map(decorator => decoratorKind(ts, checker, decorator, framework.targets));
|
|
734
|
+
const directKinds = kindsByDecorator.filter(Boolean);
|
|
735
|
+
for (const [index, decorator] of classDecorators.entries()) if (!kindsByDecorator[index]) {
|
|
736
|
+
const constructed = constructedDecoratorKind(ts, checker, decorator, framework.targets);
|
|
737
|
+
if (constructed && (NEST_CREATED.has(constructed) || MESSAGE_HANDLERS.has(constructed) || constructed.includes('framework'))) {
|
|
738
|
+
readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} decorator identity`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const classKind = directKinds.find(kind => NEST_CREATED.has(kind)) ?? null;
|
|
742
|
+
for (const member of statement.members) for (const decorated of [member, ...(ts.isConstructorDeclaration(member) ? member.parameters : [])]) {
|
|
743
|
+
for (const decorator of decorators(ts, decorated)) if (!decoratorKind(ts, checker, decorator, framework.targets)) {
|
|
744
|
+
const constructed = constructedDecoratorKind(ts, checker, decorator, framework.targets);
|
|
745
|
+
if (constructed === 'Inject' || constructed === 'unproven framework') {
|
|
746
|
+
readonlyReasons.push(`${relativePath(config.root, sourceFile.fileName)} has a constructed ${constructed} injection decorator identity`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const hasInjectedMember = statement.members.some(member => decorators(ts, member).some(decorator => decoratorKind(ts, checker, decorator, framework.targets) === 'Inject')
|
|
751
|
+
|| (ts.isConstructorDeclaration(member) && member.parameters.some(parameter => decorators(ts, parameter)
|
|
752
|
+
.some(decorator => decoratorKind(ts, checker, decorator, framework.targets) === 'Inject'))));
|
|
753
|
+
if (classKind || hasInjectedMember) {
|
|
754
|
+
injectedClasses += 1;
|
|
755
|
+
checkInjectedClass(config, context, checker, statement, classKind, framework.targets, localFiles, violations, readonlyReasons);
|
|
756
|
+
}
|
|
757
|
+
for (let index = 0; index < classDecorators.length; index += 1) {
|
|
758
|
+
const kind = kindsByDecorator[index];
|
|
759
|
+
if (!MESSAGE_HANDLERS.has(kind)) continue;
|
|
760
|
+
const message = messageClass(config, context, checker, classDecorators[index], localFiles, readonlyReasons);
|
|
761
|
+
if (!message) continue;
|
|
762
|
+
const key = `${message.getSourceFile().fileName}:${message.pos}`;
|
|
763
|
+
if (messageSeen.has(key)) continue;
|
|
764
|
+
messageSeen.add(key);
|
|
765
|
+
messages += 1;
|
|
766
|
+
checkMessageReadonly(config, context, context.checkerFor(message.getSourceFile().fileName), message, localFiles, violations, readonlyReasons);
|
|
767
|
+
}
|
|
768
|
+
if (role.role === 'use-case' && !role.transport && ts.isClassDeclaration(statement) && statement.name) {
|
|
769
|
+
const exported = exportedSymbols(ts, checker, sourceFile).some(symbol => (symbol.getDeclarations?.() ?? []).includes(statement));
|
|
770
|
+
if (!exported) continue;
|
|
771
|
+
const classSymbol = normalizedSymbol(ts, checker, statement.name);
|
|
772
|
+
const instanceType = classSymbol ? checker.getDeclaredTypeOfSymbol(classSymbol) : null;
|
|
773
|
+
const execute = instanceType ? checker.getPropertyOfType(instanceType, 'execute') : null;
|
|
774
|
+
if (!execute) publicReasons.push(`${relativePath(config.root, sourceFile.fileName)} exports a use-case class without a statically resolved execute contract`);
|
|
775
|
+
else callableSignatures += checkCallableSymbol(config, context, checker, execute, statement, sourceFile, statement.name,
|
|
776
|
+
violations, publicReasons, publicSeen, localFiles);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const publicDetails = [...new Set(publicReasons)].sort();
|
|
782
|
+
const readonlyDetails = [...new Set(readonlyReasons)].sort();
|
|
783
|
+
return {
|
|
784
|
+
violations,
|
|
785
|
+
coverage: {
|
|
786
|
+
publicContracts: publicDetails.length ? { status: 'unavailable', callableSignatures, details: publicDetails }
|
|
787
|
+
: { status: 'checked', callableSignatures },
|
|
788
|
+
readonlyBoundaries: readonlyDetails.length ? { status: 'unavailable', injectedClasses, messages, details: readonlyDetails }
|
|
789
|
+
: { status: 'checked', injectedClasses, messages },
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
}
|