@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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Specs and tests are not in the graph. The public entry of an owner is found the way dead-exports.mjs finds it.
|
|
15
15
|
*/
|
|
16
16
|
import { canonical } from './config.mjs';
|
|
17
|
+
import { hasAnyFlag } from '../../lib/ts-ast.mjs';
|
|
17
18
|
import { entryOf } from './dead-exports.mjs';
|
|
18
19
|
|
|
19
20
|
export const SYMBOL_RULE_IDS = ['HFS_DUPLICATE_SYMBOL', 'HFS_ALIAS_REEXPORT'];
|
|
@@ -21,41 +22,58 @@ export const SYMBOL_RULE_IDS = ['HFS_DUPLICATE_SYMBOL', 'HFS_ALIAS_REEXPORT'];
|
|
|
21
22
|
const lineOf = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
22
23
|
|
|
23
24
|
/** The names a source file declares at top level and exports: Map<name, line>. Re-exports from another module declare nothing. */
|
|
25
|
+
function addBindingNames(ts, sourceFile, name, into, node) {
|
|
26
|
+
if (name.kind === ts.SyntaxKind.Identifier) {
|
|
27
|
+
into.set(name.text, lineOf(sourceFile, node));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const element of name.elements) {
|
|
31
|
+
if (element.kind === ts.SyntaxKind.BindingElement) addBindingNames(ts, sourceFile, element.name, into, node);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function recordStatementDeclarations(ts, sourceFile, statement, declared, exported) {
|
|
36
|
+
const kind = ts.SyntaxKind;
|
|
37
|
+
const modifiers = statement.modifiers ?? [];
|
|
38
|
+
const publish = modifiers.some(modifier => modifier.kind === kind.ExportKeyword) && !modifiers.some(modifier => modifier.kind === kind.DefaultKeyword);
|
|
39
|
+
const into = publish ? exported : declared;
|
|
40
|
+
if (statement.kind === kind.VariableStatement) {
|
|
41
|
+
for (const declaration of statement.declarationList.declarations) addBindingNames(ts, sourceFile, declaration.name, into, declaration);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (statement.name?.kind === kind.Identifier
|
|
45
|
+
&& [kind.FunctionDeclaration, kind.ClassDeclaration, kind.EnumDeclaration, kind.TypeAliasDeclaration, kind.InterfaceDeclaration].includes(statement.kind)) {
|
|
46
|
+
into.set(statement.name.text, lineOf(sourceFile, statement));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exposeLocalExports(localLists, declared, exported) {
|
|
51
|
+
for (const element of localLists) {
|
|
52
|
+
const local = (element.propertyName ?? element.name).text;
|
|
53
|
+
if (declared.has(local)) exported.set(element.name.text, declared.get(local));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
function declaredExports(ts, sourceFile) {
|
|
25
58
|
const kind = ts.SyntaxKind;
|
|
26
59
|
const declared = new Map();
|
|
27
60
|
const exported = new Map();
|
|
28
61
|
const localLists = [];
|
|
29
|
-
const bindingNames = (name, into, node) => {
|
|
30
|
-
if (name.kind === kind.Identifier) into.set(name.text, lineOf(sourceFile, node));
|
|
31
|
-
else for (const element of name.elements) if (element.kind === kind.BindingElement) bindingNames(element.name, into, node);
|
|
32
|
-
};
|
|
33
62
|
for (const statement of sourceFile.statements) {
|
|
34
63
|
if (statement.kind === kind.ExportDeclaration) {
|
|
35
64
|
if (!statement.moduleSpecifier && statement.exportClause?.kind === kind.NamedExports) localLists.push(...statement.exportClause.elements);
|
|
36
65
|
continue;
|
|
37
66
|
}
|
|
38
|
-
|
|
39
|
-
const publish = modifiers.some(modifier => modifier.kind === kind.ExportKeyword) && !modifiers.some(modifier => modifier.kind === kind.DefaultKeyword);
|
|
40
|
-
const into = publish ? exported : declared;
|
|
41
|
-
if (statement.kind === kind.VariableStatement) {
|
|
42
|
-
for (const declaration of statement.declarationList.declarations) bindingNames(declaration.name, into, declaration);
|
|
43
|
-
} else if (statement.name?.kind === kind.Identifier
|
|
44
|
-
&& [kind.FunctionDeclaration, kind.ClassDeclaration, kind.EnumDeclaration, kind.TypeAliasDeclaration, kind.InterfaceDeclaration].includes(statement.kind)) {
|
|
45
|
-
into.set(statement.name.text, lineOf(sourceFile, statement));
|
|
46
|
-
}
|
|
67
|
+
recordStatementDeclarations(ts, sourceFile, statement, declared, exported);
|
|
47
68
|
}
|
|
48
69
|
// `export { local }` of a name declared in this file makes that declaration exported.
|
|
49
|
-
|
|
50
|
-
const local = (element.propertyName ?? element.name).text;
|
|
51
|
-
if (declared.has(local)) exported.set(element.name.text, declared.get(local));
|
|
52
|
-
}
|
|
70
|
+
exposeLocalExports(localLists, declared, exported);
|
|
53
71
|
return exported;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
/** The declaration an export of an entry resolves to: its repository-relative file and its own name, or null. */
|
|
57
75
|
function declaringFile(ts, checker, graph, symbol) {
|
|
58
|
-
const resolved = symbol.flags
|
|
76
|
+
const resolved = hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
|
|
59
77
|
for (const declaration of resolved?.declarations ?? []) {
|
|
60
78
|
const rel = graph.abs(canonical(declaration.getSourceFile().fileName));
|
|
61
79
|
if (rel) return { rel, name: resolved.getName() };
|
|
@@ -63,8 +81,7 @@ function declaringFile(ts, checker, graph, symbol) {
|
|
|
63
81
|
return null;
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
function
|
|
67
|
-
const { ts } = context;
|
|
84
|
+
function declarationIndex(ts, graph) {
|
|
68
85
|
const declarations = new Map(); // name -> Map<rel, line>
|
|
69
86
|
for (const [rel, node] of graph.files) {
|
|
70
87
|
for (const [name, line] of declaredExports(ts, node.sourceFile)) {
|
|
@@ -73,47 +90,72 @@ function duplicateSymbols({ context, graph, config }) {
|
|
|
73
90
|
declarations.get(name).set(rel, line);
|
|
74
91
|
}
|
|
75
92
|
}
|
|
93
|
+
return declarations;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function duplicateViolation(scan, symbol, owner, entry) {
|
|
97
|
+
const { ts, checker, graph, declarations, seen } = scan;
|
|
98
|
+
const declaration = declaringFile(ts, checker, graph, symbol);
|
|
99
|
+
if (!declaration || declaration.name === 'default') return null;
|
|
100
|
+
const { rel: home, name } = declaration;
|
|
101
|
+
const others = [...(declarations.get(name) ?? [])].filter(([rel]) => rel !== home);
|
|
102
|
+
if (!others.length || seen.has(`${home}\0${name}`)) return null;
|
|
103
|
+
seen.add(`${home}\0${name}`);
|
|
104
|
+
const where = others.map(([rel, at]) => `${rel}:${at}`);
|
|
105
|
+
return {
|
|
106
|
+
ruleId: 'HFS_DUPLICATE_SYMBOL',
|
|
107
|
+
path: home, line: declarations.get(name).get(home) ?? 1, column: 1, name, owner: owner.root, entry,
|
|
108
|
+
otherDeclarations: where,
|
|
109
|
+
message: `${name} is exported through the public entry ${entry} (declared in ${home}) and also declared and exported in ${where.join(', ')}; one name, one declaration: rename or delete the copy.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function inspectPublicOwner(ts, context, graph, config, declarations, seen, [key, owner]) {
|
|
114
|
+
if (owner.tier === 'app') return { violations: [], surface: 0 };
|
|
115
|
+
const entry = entryOf(graph, config, key, owner);
|
|
116
|
+
if (!entry) return { violations: [], surface: 0 };
|
|
117
|
+
const entryFile = graph.files.get(entry);
|
|
118
|
+
const checker = context.checkerFor(entryFile.abs);
|
|
119
|
+
const moduleSymbol = checker?.getSymbolAtLocation(entryFile.sourceFile);
|
|
120
|
+
if (!moduleSymbol) return { violations: [], surface: 0 };
|
|
121
|
+
const violations = [];
|
|
122
|
+
const scan = { ts, checker, graph, declarations, seen };
|
|
123
|
+
let surface = 0;
|
|
124
|
+
for (const symbol of checker.getExportsOfModule(moduleSymbol)) {
|
|
125
|
+
surface += 1;
|
|
126
|
+
const violation = duplicateViolation(scan, symbol, owner, entry);
|
|
127
|
+
if (violation) violations.push(violation);
|
|
128
|
+
}
|
|
129
|
+
return { violations, surface };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function duplicateSymbols({ context, graph, config }) {
|
|
133
|
+
const declarations = declarationIndex(context.ts, graph);
|
|
76
134
|
const violations = [];
|
|
77
135
|
const seen = new Set();
|
|
78
136
|
let surface = 0;
|
|
79
|
-
for (const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const entryFile = graph.files.get(entry);
|
|
84
|
-
const checker = context.checkerFor(entryFile.abs);
|
|
85
|
-
const moduleSymbol = checker?.getSymbolAtLocation(entryFile.sourceFile);
|
|
86
|
-
if (!moduleSymbol) continue;
|
|
87
|
-
for (const symbol of checker.getExportsOfModule(moduleSymbol)) {
|
|
88
|
-
surface += 1;
|
|
89
|
-
const declaration = declaringFile(ts, checker, graph, symbol);
|
|
90
|
-
if (!declaration || declaration.name === 'default') continue;
|
|
91
|
-
const { rel: home, name } = declaration;
|
|
92
|
-
const others = [...(declarations.get(name) ?? [])].filter(([rel]) => rel !== home);
|
|
93
|
-
if (!others.length || seen.has(`${home}\0${name}`)) continue;
|
|
94
|
-
seen.add(`${home}\0${name}`);
|
|
95
|
-
const where = others.map(([rel, at]) => `${rel}:${at}`);
|
|
96
|
-
violations.push({
|
|
97
|
-
ruleId: 'HFS_DUPLICATE_SYMBOL',
|
|
98
|
-
path: home, line: declarations.get(name).get(home) ?? 1, column: 1, name, owner: owner.root, entry,
|
|
99
|
-
otherDeclarations: where,
|
|
100
|
-
message: `${name} is exported through the public entry ${entry} (declared in ${home}) and also declared and exported in ${where.join(', ')}; one name, one declaration: rename or delete the copy.`,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
137
|
+
for (const item of graph.ownerRoots) {
|
|
138
|
+
const result = inspectPublicOwner(context.ts, context, graph, config, declarations, seen, item);
|
|
139
|
+
violations.push(...result.violations);
|
|
140
|
+
surface += result.surface;
|
|
103
141
|
}
|
|
104
142
|
return { violations, surface };
|
|
105
143
|
}
|
|
106
144
|
|
|
107
145
|
const isExported = (ts, statement) => (statement.modifiers ?? []).some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
108
146
|
&& !(statement.modifiers ?? []).some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
147
|
+
const bareAliasExpression = (ts, expression) => ts.isIdentifier(expression)
|
|
148
|
+
|| (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name) && (ts.isIdentifier(expression.expression) || ts.isPropertyAccessExpression(expression.expression)));
|
|
109
149
|
|
|
110
150
|
/** The declaration `node` (an identifier, a member or a qualified name) resolves to when a repository file declares it as one of `kinds`, else null. */
|
|
111
151
|
function repositoryDeclaration(ts, checker, graph, node, kinds) {
|
|
112
|
-
|
|
152
|
+
let target = node;
|
|
153
|
+
if (ts.isPropertyAccessExpression(node)) target = node.name;
|
|
154
|
+
else if (ts.isQualifiedName(node)) target = node.right;
|
|
113
155
|
const symbol = checker?.getSymbolAtLocation(target);
|
|
114
156
|
if (!symbol) return null;
|
|
115
|
-
const resolved = symbol.flags
|
|
116
|
-
const home = (resolved?.declarations ?? []).
|
|
157
|
+
const resolved = hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
|
|
158
|
+
const home = (resolved?.declarations ?? []).some(declaration => kinds.includes(declaration.kind) && graph.abs(canonical(declaration.getSourceFile().fileName)));
|
|
117
159
|
return home ? resolved.getName() : null;
|
|
118
160
|
}
|
|
119
161
|
|
|
@@ -122,31 +164,44 @@ function repositoryDeclaration(ts, checker, graph, node, kinds) {
|
|
|
122
164
|
* `export const Y = X` and `export const Y = X.y` (X or y a function, class, const or enum), `export type Y = X` and
|
|
123
165
|
* `export interface Y extends X {}` with no body, no type arguments and no type parameters.
|
|
124
166
|
*/
|
|
125
|
-
function
|
|
126
|
-
const kind = ts.SyntaxKind;
|
|
127
|
-
if (!checker || !isExported(ts, statement)) return [];
|
|
128
|
-
const bare = expression => ts.isIdentifier(expression) || (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name) && (ts.isIdentifier(expression.expression) || ts.isPropertyAccessExpression(expression.expression)));
|
|
167
|
+
function variableAliases(ts, checker, graph, statement, kind) {
|
|
129
168
|
const found = [];
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (of && of !== declaration.name.text) found.push({ node: declaration, name: declaration.name.text, of, form: `export const ${declaration.name.text} = ${declaration.initializer.getText()}` });
|
|
135
|
-
}
|
|
136
|
-
} else if (statement.kind === kind.TypeAliasDeclaration && !statement.typeParameters?.length && ts.isTypeReferenceNode(statement.type) && !statement.type.typeArguments?.length) {
|
|
137
|
-
const of = repositoryDeclaration(ts, checker, graph, statement.type.typeName, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration, kind.EnumDeclaration]);
|
|
138
|
-
if (of && of !== statement.name.text) found.push({ node: statement, name: statement.name.text, of, form: `export type ${statement.name.text} = ${statement.type.getText()}` });
|
|
139
|
-
} else if (statement.kind === kind.InterfaceDeclaration && !statement.members.length && !statement.typeParameters?.length) {
|
|
140
|
-
const heritage = (statement.heritageClauses ?? []).flatMap(clause => clause.types);
|
|
141
|
-
const [only] = heritage;
|
|
142
|
-
if (heritage.length === 1 && !only.typeArguments?.length && bare(only.expression)) {
|
|
143
|
-
const of = repositoryDeclaration(ts, checker, graph, only.expression, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration]);
|
|
144
|
-
if (of && of !== statement.name.text) found.push({ node: statement, name: statement.name.text, of, form: `export interface ${statement.name.text} extends ${only.expression.getText()} {}` });
|
|
145
|
-
}
|
|
169
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
170
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !bareAliasExpression(ts, declaration.initializer)) continue;
|
|
171
|
+
const of = repositoryDeclaration(ts, checker, graph, declaration.initializer, [kind.FunctionDeclaration, kind.ClassDeclaration, kind.VariableDeclaration, kind.EnumDeclaration]);
|
|
172
|
+
if (of && of !== declaration.name.text) found.push({ node: declaration, name: declaration.name.text, of, form: `export const ${declaration.name.text} = ${declaration.initializer.getText()}` });
|
|
146
173
|
}
|
|
147
174
|
return found;
|
|
148
175
|
}
|
|
149
176
|
|
|
177
|
+
function typeAliasOf(ts, checker, graph, statement, kind) {
|
|
178
|
+
if (statement.typeParameters?.length || !ts.isTypeReferenceNode(statement.type) || statement.type.typeArguments?.length) return [];
|
|
179
|
+
const of = repositoryDeclaration(ts, checker, graph, statement.type.typeName, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration, kind.EnumDeclaration]);
|
|
180
|
+
return of && of !== statement.name.text
|
|
181
|
+
? [{ node: statement, name: statement.name.text, of, form: `export type ${statement.name.text} = ${statement.type.getText()}` }]
|
|
182
|
+
: [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function interfaceAliasOf(ts, checker, graph, statement, kind) {
|
|
186
|
+
if (statement.members.length || statement.typeParameters?.length) return [];
|
|
187
|
+
const heritage = (statement.heritageClauses ?? []).flatMap(clause => clause.types);
|
|
188
|
+
const [only] = heritage;
|
|
189
|
+
if (heritage.length !== 1 || only.typeArguments?.length || !bareAliasExpression(ts, only.expression)) return [];
|
|
190
|
+
const of = repositoryDeclaration(ts, checker, graph, only.expression, [kind.InterfaceDeclaration, kind.TypeAliasDeclaration, kind.ClassDeclaration]);
|
|
191
|
+
return of && of !== statement.name.text
|
|
192
|
+
? [{ node: statement, name: statement.name.text, of, form: `export interface ${statement.name.text} extends ${only.expression.getText()} {}` }]
|
|
193
|
+
: [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function declarationAliases(ts, checker, graph, statement) {
|
|
197
|
+
const kind = ts.SyntaxKind;
|
|
198
|
+
if (!checker || !isExported(ts, statement)) return [];
|
|
199
|
+
if (statement.kind === kind.VariableStatement) return variableAliases(ts, checker, graph, statement, kind);
|
|
200
|
+
if (statement.kind === kind.TypeAliasDeclaration) return typeAliasOf(ts, checker, graph, statement, kind);
|
|
201
|
+
if (statement.kind === kind.InterfaceDeclaration) return interfaceAliasOf(ts, checker, graph, statement, kind);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
150
205
|
/** The names Next.js reads from a route segment file (layout, page, route, ...): the framework fixes them. */
|
|
151
206
|
const NEXT_SEGMENT_EXPORTS = new Set([
|
|
152
207
|
'generateMetadata', 'metadata', 'generateViewport', 'viewport', 'generateStaticParams', 'generateImageMetadata', 'generateSitemaps',
|
|
@@ -154,42 +209,63 @@ const NEXT_SEGMENT_EXPORTS = new Set([
|
|
|
154
209
|
]);
|
|
155
210
|
const ROUTE_SLOT = 'fe.route';
|
|
156
211
|
|
|
212
|
+
function isFrameworkName(node, name) {
|
|
213
|
+
return node.slot === ROUTE_SLOT && NEXT_SEGMENT_EXPORTS.has(name);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function addDeclarationAliasViolations(env, rel, node, statement, point, violations) {
|
|
217
|
+
const { ts, checker, graph } = env;
|
|
218
|
+
for (const alias of declarationAliases(ts, checker, graph, statement)) {
|
|
219
|
+
if (isFrameworkName(node, alias.name)) continue;
|
|
220
|
+
const at = point(alias.node);
|
|
221
|
+
violations.push({
|
|
222
|
+
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: alias.name, aliasOf: alias.of,
|
|
223
|
+
message: `${alias.form} in ${rel} gives the declaration ${alias.of} a second name: use ${alias.of} where ${alias.name} is used and delete ${alias.name}.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function addNamespaceAliasViolation(rel, statement, clause, point, violations) {
|
|
229
|
+
const at = point(statement);
|
|
230
|
+
violations.push({
|
|
231
|
+
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: clause.name.text,
|
|
232
|
+
message: `export * as ${clause.name.text} in ${rel} publishes a whole module under a second name: export the declarations it holds by their own names.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function addNamedAliasViolations(rel, node, clause, point, violations) {
|
|
237
|
+
for (const element of clause.elements) {
|
|
238
|
+
if (!element.propertyName || element.propertyName.text === element.name.text || isFrameworkName(node, element.name.text)) continue;
|
|
239
|
+
const at = point(element);
|
|
240
|
+
violations.push({
|
|
241
|
+
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1,
|
|
242
|
+
name: element.name.text, aliasOf: element.propertyName.text,
|
|
243
|
+
message: `export { ${element.propertyName.text} as ${element.name.text} } in ${rel} gives one declaration a second name: rename the declaration to ${element.name.text}, or import and export it as ${element.propertyName.text}.`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function addExportAliasViolations(ts, rel, node, statement, point, violations) {
|
|
249
|
+
if (statement.kind !== ts.SyntaxKind.ExportDeclaration || !statement.exportClause) return;
|
|
250
|
+
const clause = statement.exportClause;
|
|
251
|
+
if (clause.kind === ts.SyntaxKind.NamespaceExport) {
|
|
252
|
+
addNamespaceAliasViolation(rel, statement, clause, point, violations);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
addNamedAliasViolations(rel, node, clause, point, violations);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function addStatementAliasViolations(context, graph, rel, node, statement, checker, violations) {
|
|
259
|
+
const point = target => node.sourceFile.getLineAndCharacterOfPosition(target.getStart(node.sourceFile));
|
|
260
|
+
addDeclarationAliasViolations({ ts: context.ts, checker, graph }, rel, node, statement, point, violations);
|
|
261
|
+
addExportAliasViolations(context.ts, rel, node, statement, point, violations);
|
|
262
|
+
}
|
|
263
|
+
|
|
157
264
|
function aliasReexports({ context, graph }) {
|
|
158
|
-
const kind = context.ts.SyntaxKind;
|
|
159
265
|
const violations = [];
|
|
160
266
|
for (const [rel, node] of graph.files) {
|
|
161
|
-
const frameworkName = name => node.slot === ROUTE_SLOT && NEXT_SEGMENT_EXPORTS.has(name);
|
|
162
|
-
const point = target => node.sourceFile.getLineAndCharacterOfPosition(target.getStart(node.sourceFile));
|
|
163
267
|
const checker = context.checkerFor(node.abs);
|
|
164
|
-
for (const statement of node.sourceFile.statements)
|
|
165
|
-
for (const alias of declarationAliases(context.ts, checker, graph, statement)) {
|
|
166
|
-
if (frameworkName(alias.name)) continue;
|
|
167
|
-
const at = point(alias.node);
|
|
168
|
-
violations.push({
|
|
169
|
-
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: alias.name, aliasOf: alias.of,
|
|
170
|
-
message: `${alias.form} in ${rel} gives the declaration ${alias.of} a second name: use ${alias.of} where ${alias.name} is used and delete ${alias.name}.`,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
if (statement.kind !== kind.ExportDeclaration || !statement.exportClause) continue;
|
|
174
|
-
const clause = statement.exportClause;
|
|
175
|
-
if (clause.kind === kind.NamespaceExport) {
|
|
176
|
-
const at = point(statement);
|
|
177
|
-
violations.push({
|
|
178
|
-
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1, name: clause.name.text,
|
|
179
|
-
message: `export * as ${clause.name.text} in ${rel} publishes a whole module under a second name: export the declarations it holds by their own names.`,
|
|
180
|
-
});
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
for (const element of clause.elements) {
|
|
184
|
-
if (!element.propertyName || element.propertyName.text === element.name.text || frameworkName(element.name.text)) continue;
|
|
185
|
-
const at = point(element);
|
|
186
|
-
violations.push({
|
|
187
|
-
ruleId: 'HFS_ALIAS_REEXPORT', path: rel, line: at.line + 1, column: at.character + 1,
|
|
188
|
-
name: element.name.text, aliasOf: element.propertyName.text,
|
|
189
|
-
message: `export { ${element.propertyName.text} as ${element.name.text} } in ${rel} gives one declaration a second name: rename the declaration to ${element.name.text}, or import and export it as ${element.propertyName.text}.`,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
268
|
+
for (const statement of node.sourceFile.statements) addStatementAliasViolations(context, graph, rel, node, statement, checker, violations);
|
|
193
269
|
}
|
|
194
270
|
return { violations };
|
|
195
271
|
}
|
|
@@ -3,7 +3,8 @@ import { allowsFile } from '../allows.mjs';
|
|
|
3
3
|
import { locateDeclaration } from '../slots.mjs';
|
|
4
4
|
import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
|
|
5
5
|
import { unwrapEach } from './ast-walks.mjs';
|
|
6
|
-
import { nameText, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
6
|
+
import { hasAnyFlag, nameText, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
7
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* R47 `test-world-files` (BE_TEST_TOPOLOGY). Judgements over the test world, read through slots, the repository's own
|
|
@@ -59,7 +60,7 @@ function defineLiteral(ts, expression) {
|
|
|
59
60
|
function configLiteral(ts, sourceFile) {
|
|
60
61
|
for (const statement of sourceFile.statements) {
|
|
61
62
|
if (!ts.isVariableStatement(statement) || !isExported(ts, statement)) continue;
|
|
62
|
-
if ((statement.declarationList.flags
|
|
63
|
+
if (!hasAnyFlag(statement.declarationList.flags, ts.NodeFlags.Const)) continue;
|
|
63
64
|
for (const declaration of statement.declarationList.declarations) {
|
|
64
65
|
const literal = declaration.initializer ? defineLiteral(ts, declaration.initializer) : null;
|
|
65
66
|
if (literal) return literal;
|
|
@@ -73,7 +74,7 @@ function environmentOf(ts, literal) {
|
|
|
73
74
|
const node = literal ? propertyOf(ts, literal, 'stack') : null;
|
|
74
75
|
const value = node ? unwrap(ts, node) : null;
|
|
75
76
|
if (!value || !ts.isStringLiteralLike(value)) return null;
|
|
76
|
-
return value.text.split(/[\\/]+/u).filter(Boolean).
|
|
77
|
+
return value.text.split(/[\\/]+/u).filter(Boolean).findLast(Boolean) ?? null;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/** The keys of the declaration's `fakes` object literal, with their positions. */
|
|
@@ -86,57 +87,63 @@ function fakeEntriesOf(ts, sourceFile, literal) {
|
|
|
86
87
|
.filter(entry => entry.name);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** The `fakedBy` entry of one `stacks` property, or null when the property does not declare one. */
|
|
91
|
+
function fakedByEntry(ts, sourceFile, property) {
|
|
92
|
+
if (!ts.isPropertyAssignment(property)) return null;
|
|
93
|
+
const body = unwrap(ts, property.initializer);
|
|
94
|
+
if (!body || !ts.isObjectLiteralExpression(body)) return null;
|
|
95
|
+
const fake = propertyOf(ts, body, 'fakedBy');
|
|
96
|
+
if (!fake) return null;
|
|
97
|
+
const reason = propertyOf(ts, body, 'reason');
|
|
98
|
+
return {
|
|
99
|
+
service: nameText(ts, property.name),
|
|
100
|
+
fake: ts.isStringLiteralLike(fake) ? fake.text : '',
|
|
101
|
+
reason: reason && ts.isStringLiteralLike(reason) ? reason.text.trim() : '',
|
|
102
|
+
...sourceLocation(sourceFile, property),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
/** The `stacks` entries that declare `fakedBy`: [{service, fake, reason, line, column}]; a value the reader cannot read statically has an empty fake and reason. */
|
|
90
107
|
function fakedByOf(ts, sourceFile, literal) {
|
|
91
108
|
const node = literal ? propertyOf(ts, literal, 'stacks') : null;
|
|
92
109
|
const value = node ? unwrap(ts, node) : null;
|
|
93
110
|
if (!value || !ts.isObjectLiteralExpression(value)) return [];
|
|
94
|
-
|
|
95
|
-
for (const property of value.properties) {
|
|
96
|
-
if (!ts.isPropertyAssignment(property)) continue;
|
|
97
|
-
const body = unwrap(ts, property.initializer);
|
|
98
|
-
if (!body || !ts.isObjectLiteralExpression(body)) continue;
|
|
99
|
-
const fake = propertyOf(ts, body, 'fakedBy');
|
|
100
|
-
if (!fake) continue;
|
|
101
|
-
const reason = propertyOf(ts, body, 'reason');
|
|
102
|
-
entries.push({
|
|
103
|
-
service: nameText(ts, property.name),
|
|
104
|
-
fake: ts.isStringLiteralLike(fake) ? fake.text : '',
|
|
105
|
-
reason: reason && ts.isStringLiteralLike(reason) ? reason.text.trim() : '',
|
|
106
|
-
...sourceLocation(sourceFile, property),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return entries;
|
|
111
|
+
return value.properties.map(property => fakedByEntry(ts, sourceFile, property)).filter(Boolean);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
114
|
+
function registerFakeProvider(fakeProviders, relative, file) {
|
|
115
|
+
if (!relative.startsWith(FAKES_DIRECTORY)) return;
|
|
116
|
+
const [provider, ...rest] = relative.slice(FAKES_DIRECTORY.length).split('/');
|
|
117
|
+
if (rest.length > 0 && !fakeProviders.has(provider)) fakeProviders.set(provider, file);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isRoleFileAtRoot(relative, suffixes) {
|
|
121
|
+
const parts = relative.slice(0, -'.ts'.length).split('.');
|
|
122
|
+
return !relative.includes('/') && parts.length >= 2 && parts.every(part => KEBAB.test(part)) && suffixes.includes(parts.at(-1));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectWorldFiles(resolver, tree, suffixes, violations) {
|
|
116
126
|
const fakeProviders = new Map();
|
|
117
127
|
let worldRoot = null;
|
|
118
128
|
let files = 0;
|
|
119
|
-
for (const file of [...tree.files].sort()) {
|
|
129
|
+
for (const file of [...tree.files].sort(byCodeUnit)) {
|
|
120
130
|
if (!file.endsWith('.ts')) continue;
|
|
121
131
|
const verdict = allowsFile(resolver, file);
|
|
122
|
-
if (
|
|
132
|
+
if (verdict?.slot !== WORLD_SLOT) continue;
|
|
123
133
|
files += 1;
|
|
124
134
|
worldRoot ??= file.slice(0, file.length - verdict.relative.length);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (rest.length > 0 && !fakeProviders.has(provider)) fakeProviders.set(provider, file);
|
|
128
|
-
}
|
|
129
|
-
if (verdict.allowed) continue;
|
|
130
|
-
const parts = verdict.relative.slice(0, -'.ts'.length).split('.');
|
|
131
|
-
const roleFileAtRoot = !verdict.relative.includes('/') && parts.length >= 2 && parts.every(part => KEBAB.test(part)) && suffixes.includes(parts.at(-1));
|
|
132
|
-
if (roleFileAtRoot) continue;
|
|
135
|
+
registerFakeProvider(fakeProviders, verdict.relative, file);
|
|
136
|
+
if (verdict.allowed || isRoleFileAtRoot(verdict.relative, suffixes)) continue;
|
|
133
137
|
violations.push({
|
|
134
138
|
ruleId: RULE, path: file, line: 1, column: 1,
|
|
135
139
|
message: `${file} is not allowed in the test world; src/tests/world/ holds only ${verdict.allows.join(', ')} and role-suffixed files (<name>.<role>.ts) at its root. Move it to fakes/ or kit/ (be.tests.world.kit), give it a role suffix, or delete it.`,
|
|
136
140
|
slot: verdict.slot,
|
|
137
141
|
});
|
|
138
142
|
}
|
|
143
|
+
return { fakeProviders, worldRoot, files };
|
|
144
|
+
}
|
|
139
145
|
|
|
146
|
+
function worldDeclaration(config, graph, ts, worldRoot, violations) {
|
|
140
147
|
const configPath = worldRoot === null ? null : `${worldRoot}${CONFIG_FILE}`;
|
|
141
148
|
const configFile = configPath === null ? null : graph.files.get(configPath) ?? null;
|
|
142
149
|
const literal = configFile ? configLiteral(ts, configFile.sourceFile) : null;
|
|
@@ -146,7 +153,10 @@ export function checkTestWorldFiles(input) {
|
|
|
146
153
|
message: `${configPath} declares no world the machine can read. Declare it in the one named form \`${NAMED_FORM}\` (an exported const of a ${DEFINE} call with an object literal, imported from @starci/test-world); a default export is refused by R89 and is not read.`,
|
|
147
154
|
});
|
|
148
155
|
}
|
|
149
|
-
|
|
156
|
+
return { configPath, configFile, literal };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function stackServices(config, environments) {
|
|
150
160
|
const services = [];
|
|
151
161
|
for (const environment of environments) {
|
|
152
162
|
let stack = null;
|
|
@@ -154,11 +164,15 @@ export function checkTestWorldFiles(input) {
|
|
|
154
164
|
try { stack = readStack({ root: locateDeclaration(config.root).appRoot, environment }); } catch { stack = null; }
|
|
155
165
|
for (const service of stack?.services ?? []) if (service.role !== 'service' && !services.some(known => known.name === service.name)) services.push({ ...service, environment });
|
|
156
166
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
return services;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function statefulWhy(service) {
|
|
171
|
+
if (STATEFUL_KINDS.has(service.kind)) return `a ${service.kind} holds data the app reads back`;
|
|
172
|
+
return service.persistent ? 'the stack gives it a persistent volume' : null;
|
|
173
|
+
}
|
|
161
174
|
|
|
175
|
+
function reportDeclaredFakeProblems(declared, services, fakeProviders, fakeEntries, configPath, stackHint, violations) {
|
|
162
176
|
for (const entry of declared) {
|
|
163
177
|
const service = services.find(candidate => candidate.name === entry.service);
|
|
164
178
|
const problems = [];
|
|
@@ -173,21 +187,48 @@ export function checkTestWorldFiles(input) {
|
|
|
173
187
|
});
|
|
174
188
|
}
|
|
175
189
|
}
|
|
190
|
+
}
|
|
176
191
|
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
function worldFakes(fakeProviders, fakeEntries, configPath) {
|
|
193
|
+
return [
|
|
179
194
|
...[...fakeProviders].map(([provider, file]) => ({ name: provider, path: file, line: 1, column: 1, what: `fakes/${provider}/` })),
|
|
180
195
|
...fakeEntries.filter(entry => !fakeProviders.has(entry.name)).map(entry => ({ name: entry.name, path: configPath, line: entry.line, column: entry.column, what: `the fakes entry ${entry.name}` })),
|
|
181
196
|
].sort((a, b) => a.name.localeCompare(b.name));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function reportServiceFakes(fakes, services, declared, configPath, stackHint, violations) {
|
|
182
200
|
for (const fake of fakes) {
|
|
183
201
|
const service = services.find(candidate => namesOfService(candidate).includes(fake.name));
|
|
184
202
|
if (!service) continue;
|
|
185
203
|
if (declared.some(entry => entry.service === service.name && entry.fake === fake.name)) continue; // judged on its stacks entry above
|
|
186
204
|
const why = statefulWhy(service);
|
|
205
|
+
let reason = `${service.name} is stateful (${why}), so no exception applies. `;
|
|
206
|
+
if (!why) {
|
|
207
|
+
const worldConfig = configPath ?? `${CONFIG_FILE} in the world`;
|
|
208
|
+
reason = `Only stateless compute that needs special hardware or an external model may be faked, and only when ${worldConfig} declares it in its stacks entry ({ fakedBy, reason }). `;
|
|
209
|
+
}
|
|
187
210
|
violations.push({
|
|
188
211
|
ruleId: RULE, path: fake.path, line: fake.line, column: fake.column, slot: WORLD_SLOT,
|
|
189
|
-
message: `${fake.what} fakes ${service.name} (${service.image}), which ${stackHint} declares: a service of the repository's own stack runs real in the test world. ${
|
|
212
|
+
message: `${fake.what} fakes ${service.name} (${service.image}), which ${stackHint} declares: a service of the repository's own stack runs real in the test world. ${reason}Delete the fake and let the world run the real service; fakes are otherwise only for external SaaS the team does not operate.`,
|
|
190
213
|
});
|
|
191
214
|
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function checkTestWorldFiles(input) {
|
|
218
|
+
const { config, graph, ts, resolver, tree } = checkerScope(input);
|
|
219
|
+
const { suffixes } = resolver.ruleParams();
|
|
220
|
+
const violations = [];
|
|
221
|
+
const { fakeProviders, worldRoot, files } = collectWorldFiles(resolver, tree, suffixes, violations);
|
|
222
|
+
const { configPath, configFile, literal } = worldDeclaration(config, graph, ts, worldRoot, violations);
|
|
223
|
+
const environments = [(literal ? environmentOf(ts, literal) : null) ?? defaultEnvironment];
|
|
224
|
+
const services = stackServices(config, environments);
|
|
225
|
+
const stackHint = `${STACKS_DIRECTORY}/${environments.join(', ')}`;
|
|
226
|
+
const declared = literal ? fakedByOf(ts, configFile.sourceFile, literal) : [];
|
|
227
|
+
const fakeEntries = literal ? fakeEntriesOf(ts, configFile.sourceFile, literal) : [];
|
|
228
|
+
reportDeclaredFakeProblems(declared, services, fakeProviders, fakeEntries, configPath, stackHint, violations);
|
|
229
|
+
|
|
230
|
+
// Every fake the world runs: the repository's fakes/<provider>/ folders and the fakes entries of the declaration.
|
|
231
|
+
const fakes = worldFakes(fakeProviders, fakeEntries, configPath);
|
|
232
|
+
reportServiceFakes(fakes, services, declared, configPath, stackHint, violations);
|
|
192
233
|
return { violations, coverage: { status: 'checked', files } };
|
|
193
234
|
}
|