@starci/hfs 4.0.9 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +247 -273
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- 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 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/lib.mjs +6 -0
- 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 +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- 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 +3 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +90 -45
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- 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/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- 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 +103 -2
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
|
@@ -17,57 +17,66 @@ export const CROSS_APP_DUPLICATE_RULE_IDS = ['FE_CROSS_APP_DUPLICATE'];
|
|
|
17
17
|
const RULE = 'FE_CROSS_APP_DUPLICATE';
|
|
18
18
|
const SOURCE = /\.[cm]?tsx?$/u;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const { SyntaxKind } = ts;
|
|
23
|
-
const resolver = graph.resolver;
|
|
24
|
-
const declaredApps = resolver.repo.apps.map(app => app.name);
|
|
25
|
-
/** The app a file belongs to: the slot binding when a slot owns it, else the declared app whose directory holds it. */
|
|
26
|
-
const appOf = rel => resolver.classifyPath(rel).bindings?.app ?? declaredApps.find(name => rel.startsWith(`apps/${name}/`)) ?? null;
|
|
27
|
-
const onlyModuleEdges = file => file.sourceFile.statements.every(statement => ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
|
|
28
|
-
|| (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)));
|
|
20
|
+
/** The app a file belongs to: the slot binding when a slot owns it, else the declared app whose directory holds it. */
|
|
21
|
+
const appOf = (resolver, declaredApps, rel) => resolver.classifyPath(rel).bindings?.app ?? declaredApps.find(name => rel.startsWith(`apps/${name}/`)) ?? null;
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
const onlyModuleEdges = (ts, file) => file.sourceFile.statements.every(statement => ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
|
|
24
|
+
|| (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)));
|
|
25
|
+
|
|
26
|
+
const leafText = (SyntaxKind, node, sourceFile) => (node.kind === SyntaxKind.JsxText ? node.getText(sourceFile).replace(/\s+/gu, ' ').trim() : node.getText(sourceFile));
|
|
27
|
+
|
|
28
|
+
function collectTokens(SyntaxKind, sourceFile, node, parts) {
|
|
29
|
+
if (node.kind >= SyntaxKind.FirstJSDocNode && node.kind <= SyntaxKind.LastJSDocNode) return;
|
|
30
|
+
if (node.kind === SyntaxKind.JsxText && node.containsOnlyTriviaWhiteSpaces) return;
|
|
31
|
+
const children = node.getChildren(sourceFile);
|
|
32
|
+
if (!children.length) {
|
|
33
|
+
if (node.kind !== SyntaxKind.EndOfFileToken) parts.push(leafText(SyntaxKind, node, sourceFile));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const child of children) collectTokens(SyntaxKind, sourceFile, child, parts);
|
|
37
|
+
}
|
|
45
38
|
|
|
39
|
+
const tokenText = (SyntaxKind, sourceFile) => {
|
|
40
|
+
const parts = [];
|
|
41
|
+
collectTokens(SyntaxKind, sourceFile, sourceFile, parts);
|
|
42
|
+
return parts.join('\u0000');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Groups the comparable files by the hash of their token text; returns the groups and the number of files compared. */
|
|
46
|
+
function groupByContent(graph, ts, declaredApps) {
|
|
46
47
|
const groups = new Map();
|
|
47
48
|
let files = 0;
|
|
48
49
|
for (const file of graph.files.values()) {
|
|
49
50
|
if (!SOURCE.test(file.rel) || /\.d\.[cm]?tsx?$/u.test(file.rel)) continue;
|
|
50
|
-
const app = appOf(file.rel);
|
|
51
|
-
if (!app || onlyModuleEdges(file) || !file.sourceFile.statements.length) continue;
|
|
51
|
+
const app = appOf(graph.resolver, declaredApps, file.rel);
|
|
52
|
+
if (!app || onlyModuleEdges(ts, file) || !file.sourceFile.statements.length) continue;
|
|
52
53
|
files += 1;
|
|
53
|
-
const key = createHash('sha256').update(
|
|
54
|
+
const key = createHash('sha256').update(tokenText(ts.SyntaxKind, file.sourceFile)).digest('hex');
|
|
54
55
|
if (!groups.has(key)) groups.set(key, []);
|
|
55
56
|
groups.get(key).push({ rel: file.rel, app });
|
|
56
57
|
}
|
|
58
|
+
return { groups, files };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const duplicateViolation = (group, item) => {
|
|
62
|
+
const twins = group.filter(other => other.app !== item.app).map(other => other.rel);
|
|
63
|
+
return {
|
|
64
|
+
ruleId: RULE, path: item.rel, line: 1, column: 1, twins,
|
|
65
|
+
message: `${item.rel} is token-for-token the same file as ${twins.join(', ')} in another app (comments and whitespace ignored); shared code is a packages/<pkg> slot. Move it to a package (packages/<pkg>) and let each app re-export it (${path.posix.basename(item.rel)} keeps only an import or export statement).`,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
57
68
|
|
|
69
|
+
export function checkCrossAppDuplicate({ graph, context }) {
|
|
70
|
+
const ts = context.ts;
|
|
71
|
+
const declaredApps = graph.resolver.repo.apps.map(app => app.name);
|
|
72
|
+
const { groups, files } = groupByContent(graph, ts, declaredApps);
|
|
58
73
|
const violations = [];
|
|
59
74
|
let duplicated = 0;
|
|
60
75
|
for (const group of groups.values()) {
|
|
61
76
|
if (new Set(group.map(item => item.app)).size < 2) continue;
|
|
62
77
|
duplicated += 1;
|
|
63
78
|
group.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
64
|
-
for (const item of group)
|
|
65
|
-
const twins = group.filter(other => other.app !== item.app).map(other => other.rel);
|
|
66
|
-
violations.push({
|
|
67
|
-
ruleId: RULE, path: item.rel, line: 1, column: 1, twins,
|
|
68
|
-
message: `${item.rel} is token-for-token the same file as ${twins.join(', ')} in another app (comments and whitespace ignored); shared code is a packages/<pkg> slot. Move it to a package (packages/<pkg>) and let each app re-export it (${path.posix.basename(item.rel)} keeps only an import or export statement).`,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
79
|
+
for (const item of group) violations.push(duplicateViolation(group, item));
|
|
71
80
|
}
|
|
72
81
|
return { violations, coverage: { status: 'checked', files, duplicatedGroups: duplicated } };
|
|
73
82
|
}
|
|
@@ -62,65 +62,104 @@ function extraEntries(graph, entry) {
|
|
|
62
62
|
return (found.slot ? graph.resolver.slot(found.slot)?.entries ?? [] : []).map(name => `${found.root}/${name}`).filter(rel => graph.files.has(rel));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const importBindings = (ts, declaration) => {
|
|
66
|
+
const clause = declaration.importClause;
|
|
67
|
+
if (!clause) return { all: false, pairs: [] }; // side-effect import: uses nothing
|
|
68
|
+
const pairs = [];
|
|
69
|
+
if (clause.name) pairs.push({ imported: 'default', exported: null });
|
|
70
|
+
const named = clause.namedBindings;
|
|
71
|
+
if (named && named.kind === ts.SyntaxKind.NamespaceImport) return { all: true, pairs: [] };
|
|
72
|
+
if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
|
|
73
|
+
return { all: false, pairs };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const exportBindings = (ts, declaration) => {
|
|
77
|
+
const clause = declaration.exportClause;
|
|
78
|
+
if (!clause || clause.kind === ts.SyntaxKind.NamespaceExport) return { all: true, pairs: [] };
|
|
79
|
+
return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const importTypeBindings = (ts, declaration) => {
|
|
83
|
+
let qualifier = declaration.qualifier;
|
|
84
|
+
if (!qualifier) return { all: true, pairs: [] };
|
|
85
|
+
while (qualifier.kind === ts.SyntaxKind.QualifiedName) qualifier = qualifier.left;
|
|
86
|
+
return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
|
|
87
|
+
};
|
|
88
|
+
|
|
65
89
|
/** Names an import/export declaration takes from its target: {all} or {pairs: [{imported, exported}]}. */
|
|
66
90
|
function bindingsOf(ts, declaration) {
|
|
67
|
-
const kind = ts.SyntaxKind;
|
|
68
91
|
if (!declaration) return { all: true, pairs: [] };
|
|
69
|
-
if (declaration.kind ===
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const pairs = [];
|
|
73
|
-
if (clause.name) pairs.push({ imported: 'default', exported: null });
|
|
74
|
-
const named = clause.namedBindings;
|
|
75
|
-
if (named && named.kind === kind.NamespaceImport) return { all: true, pairs: [] };
|
|
76
|
-
if (named) for (const element of named.elements) pairs.push({ imported: (element.propertyName ?? element.name).text, exported: null });
|
|
77
|
-
return { all: false, pairs };
|
|
78
|
-
}
|
|
79
|
-
if (declaration.kind === kind.ExportDeclaration) {
|
|
80
|
-
const clause = declaration.exportClause;
|
|
81
|
-
if (!clause || clause.kind === kind.NamespaceExport) return { all: true, pairs: [] };
|
|
82
|
-
return { all: false, pairs: clause.elements.map(element => ({ imported: (element.propertyName ?? element.name).text, exported: element.name.text })) };
|
|
83
|
-
}
|
|
84
|
-
if (declaration.kind === kind.ImportTypeNode) {
|
|
85
|
-
let qualifier = declaration.qualifier;
|
|
86
|
-
if (!qualifier) return { all: true, pairs: [] };
|
|
87
|
-
while (qualifier.kind === kind.QualifiedName) qualifier = qualifier.left;
|
|
88
|
-
return { all: false, pairs: [{ imported: qualifier.text, exported: null }] };
|
|
89
|
-
}
|
|
92
|
+
if (declaration.kind === ts.SyntaxKind.ImportDeclaration) return importBindings(ts, declaration);
|
|
93
|
+
if (declaration.kind === ts.SyntaxKind.ExportDeclaration) return exportBindings(ts, declaration);
|
|
94
|
+
if (declaration.kind === ts.SyntaxKind.ImportTypeNode) return importTypeBindings(ts, declaration);
|
|
90
95
|
// ImportEqualsDeclaration, dynamic import(), require(): the whole module is taken.
|
|
91
96
|
return { all: true, pairs: [] };
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
const addExportName = (found, sourceFile, name, node) => {
|
|
100
|
+
if (found.has(name)) return;
|
|
101
|
+
found.set(name, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const addBindingNames = (ts, found, sourceFile, name, node) => {
|
|
105
|
+
if (name.kind === ts.SyntaxKind.Identifier) addExportName(found, sourceFile, name.text, node);
|
|
106
|
+
else for (const element of name.elements) if (element.kind === ts.SyntaxKind.BindingElement) addBindingNames(ts, found, sourceFile, element.name, node);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const addExportDeclarationNames = (ts, statement, add) => {
|
|
110
|
+
if (statement.exportClause?.kind === ts.SyntaxKind.NamedExports) {
|
|
111
|
+
for (const element of statement.exportClause.elements) add(element.name.text, element);
|
|
112
|
+
} else if (statement.exportClause?.kind === ts.SyntaxKind.NamespaceExport) add(statement.exportClause.name.text, statement);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const addStatementNames = (ts, found, sourceFile, statement) => {
|
|
116
|
+
const kind = ts.SyntaxKind;
|
|
117
|
+
const add = (name, node) => addExportName(found, sourceFile, name, node);
|
|
118
|
+
if (statement.kind === kind.ExportDeclaration) return addExportDeclarationNames(ts, statement, add);
|
|
119
|
+
if (statement.kind === kind.ExportAssignment) return add('default', statement);
|
|
120
|
+
const modifiers = statement.modifiers ?? [];
|
|
121
|
+
if (!modifiers.some(modifier => modifier.kind === kind.ExportKeyword)) return undefined;
|
|
122
|
+
if (modifiers.some(modifier => modifier.kind === kind.DefaultKeyword)) return add('default', statement);
|
|
123
|
+
if (statement.kind === kind.VariableStatement) {
|
|
124
|
+
for (const declaration of statement.declarationList.declarations) addBindingNames(ts, found, sourceFile, declaration.name, declaration);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return statement.name && statement.name.kind === kind.Identifier ? add(statement.name.text, statement) : undefined;
|
|
128
|
+
};
|
|
129
|
+
|
|
94
130
|
/** The exported names of an entry source file, each with the line of its declaration. */
|
|
95
131
|
export function exportedNames(ts, sourceFile) {
|
|
96
|
-
const kind = ts.SyntaxKind;
|
|
97
132
|
const found = new Map();
|
|
98
|
-
const
|
|
99
|
-
if (found.has(name)) return;
|
|
100
|
-
found.set(name, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
|
|
101
|
-
};
|
|
102
|
-
const bindingNames = (name, node) => {
|
|
103
|
-
if (name.kind === kind.Identifier) add(name.text, node);
|
|
104
|
-
else for (const element of name.elements) if (element.kind === kind.BindingElement) bindingNames(element.name, node);
|
|
105
|
-
};
|
|
106
|
-
for (const statement of sourceFile.statements) {
|
|
107
|
-
if (statement.kind === kind.ExportDeclaration) {
|
|
108
|
-
if (statement.exportClause?.kind === kind.NamedExports) for (const element of statement.exportClause.elements) add(element.name.text, element);
|
|
109
|
-
else if (statement.exportClause?.kind === kind.NamespaceExport) add(statement.exportClause.name.text, statement);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (statement.kind === kind.ExportAssignment) { add('default', statement); continue; }
|
|
113
|
-
const modifiers = statement.modifiers ?? [];
|
|
114
|
-
if (!modifiers.some(modifier => modifier.kind === kind.ExportKeyword)) continue;
|
|
115
|
-
if (modifiers.some(modifier => modifier.kind === kind.DefaultKeyword)) { add('default', statement); continue; }
|
|
116
|
-
if (statement.kind === kind.VariableStatement) for (const declaration of statement.declarationList.declarations) bindingNames(declaration.name, declaration);
|
|
117
|
-
else if (statement.name && statement.name.kind === kind.Identifier) add(statement.name.text, statement);
|
|
118
|
-
}
|
|
133
|
+
for (const statement of sourceFile.statements) addStatementNames(ts, found, sourceFile, statement);
|
|
119
134
|
return found;
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
const withoutSlash = value => value.replace(/\/$/, '');
|
|
123
138
|
|
|
139
|
+
const addConfigTextRoots = (graph, directory, text, roots) => {
|
|
140
|
+
for (const match of text.matchAll(CONFIG_STRING)) {
|
|
141
|
+
const target = path.posix.join(directory, match[1]);
|
|
142
|
+
const hit = CONFIG_EXTENSIONS.map(extension => `${target}${extension}`).find(candidate => graph.files.has(candidate));
|
|
143
|
+
if (hit) roots.add(hit);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const addAppConfigRoots = (graph, config, app, roots) => {
|
|
148
|
+
const directory = `apps/${app.name}`;
|
|
149
|
+
let names = [];
|
|
150
|
+
try { names = fs.readdirSync(path.join(config.root, ...directory.split('/'))); } catch { return; }
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
if (graph.resolver.classifyPath(`${directory}/${name}`).slot !== 'fe.app.next' || !/\.[cm]?[jt]sx?$/.test(name)) continue;
|
|
153
|
+
let text = '';
|
|
154
|
+
try { text = fs.readFileSync(path.join(config.root, ...directory.split('/'), name), 'utf8'); } catch { continue; }
|
|
155
|
+
addConfigTextRoots(graph, directory, text, roots);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const addFrameworkConfigRoots = (graph, config, roots) => {
|
|
160
|
+
for (const app of config.apps ?? []) addAppConfigRoots(graph, config, app, roots);
|
|
161
|
+
};
|
|
162
|
+
|
|
124
163
|
/** The files the graph serves from: what an app slot requires, every route file, every package entry, every config-named file. */
|
|
125
164
|
function rootFiles(graph, config) {
|
|
126
165
|
const roots = new Set();
|
|
@@ -129,7 +168,7 @@ function rootFiles(graph, config) {
|
|
|
129
168
|
if (!node.owner || node.tier !== 'app') continue;
|
|
130
169
|
const slot = graph.resolver.slot(node.owner.slot);
|
|
131
170
|
const inside = rel.slice(withoutSlash(node.owner.root).length + 1);
|
|
132
|
-
if ((slot?.requires ?? []).
|
|
171
|
+
if ((slot?.requires ?? []).includes(inside)) roots.add(rel);
|
|
133
172
|
}
|
|
134
173
|
for (const [key, owner] of graph.ownerRoots) {
|
|
135
174
|
if (owner.tier !== 'package') continue;
|
|
@@ -137,49 +176,29 @@ function rootFiles(graph, config) {
|
|
|
137
176
|
if (entry) roots.add(entry);
|
|
138
177
|
}
|
|
139
178
|
// A framework config of an app (slot fe.app.next) names sources by string, never by import.
|
|
140
|
-
|
|
141
|
-
const directory = `apps/${app.name}`;
|
|
142
|
-
let names = [];
|
|
143
|
-
try { names = fs.readdirSync(path.join(config.root, ...directory.split('/'))); } catch { continue; }
|
|
144
|
-
for (const name of names) {
|
|
145
|
-
if (graph.resolver.classifyPath(`${directory}/${name}`).slot !== 'fe.app.next' || !/\.[cm]?[jt]sx?$/.test(name)) continue;
|
|
146
|
-
let text = '';
|
|
147
|
-
try { text = fs.readFileSync(path.join(config.root, ...directory.split('/'), name), 'utf8'); } catch { continue; }
|
|
148
|
-
for (const match of text.matchAll(CONFIG_STRING)) {
|
|
149
|
-
const target = path.posix.join(directory, match[1]);
|
|
150
|
-
const hit = CONFIG_EXTENSIONS.map(extension => `${target}${extension}`).find(candidate => graph.files.has(candidate));
|
|
151
|
-
if (hit) roots.add(hit);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
179
|
+
addFrameworkConfigRoots(graph, config, roots);
|
|
155
180
|
return roots;
|
|
156
181
|
}
|
|
157
182
|
|
|
183
|
+
const unusedFileViolation = (rel, node) => ({
|
|
184
|
+
ruleId: 'HFS_UNUSED_FILE',
|
|
185
|
+
path: rel, line: 1, column: 1,
|
|
186
|
+
slot: node.slot, owner: node.owner?.root ?? null,
|
|
187
|
+
message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a route file, a package entry): nothing imports it, so no process runs it. Delete the file, or import it where it is used (a spec alone does not count).`,
|
|
188
|
+
});
|
|
189
|
+
|
|
158
190
|
/** HFS_UNUSED_FILE: production files no root reaches through runtime or type-only imports and re-exports. */
|
|
159
191
|
function deadFiles(graph, config) {
|
|
160
192
|
const roots = rootFiles(graph, config);
|
|
161
|
-
const forward =
|
|
162
|
-
for (const edge of graph.edges) {
|
|
163
|
-
if (!forward.has(edge.from)) forward.set(edge.from, []);
|
|
164
|
-
forward.get(edge.from).push(edge.to);
|
|
165
|
-
}
|
|
193
|
+
const forward = Map.groupBy(graph.edges, edge => edge.from);
|
|
166
194
|
const reached = new Set(roots);
|
|
167
195
|
const queue = [...roots];
|
|
168
|
-
while (queue.length) for (const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (reached.has(rel)) continue;
|
|
175
|
-
violations.push({
|
|
176
|
-
ruleId: 'HFS_UNUSED_FILE',
|
|
177
|
-
path: rel, line: 1, column: 1,
|
|
178
|
-
slot: node.slot, owner: node.owner?.root ?? null,
|
|
179
|
-
message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a route file, a package entry): nothing imports it, so no process runs it. Delete the file, or import it where it is used (a spec alone does not count).`,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
return { violations, judged, roots: roots.size };
|
|
196
|
+
while (queue.length) for (const { to } of forward.get(queue.shift()) ?? []) if (!reached.has(to)) { reached.add(to); queue.push(to); }
|
|
197
|
+
const judgedFiles = [...graph.files]
|
|
198
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
199
|
+
.filter(([, node]) => !(node.tier === null || UNJUDGED_TIERS.has(node.tier) || node.tier === 'route'));
|
|
200
|
+
const violations = judgedFiles.filter(([rel]) => !reached.has(rel)).map(([rel, node]) => unusedFileViolation(rel, node));
|
|
201
|
+
return { violations, judged: judgedFiles.length, roots: roots.size };
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
/** The unit spec of a webhook, gateway or subscription door: a door has no service of its own, so its spec is the only unit spec of its kind. */
|
|
@@ -214,75 +233,113 @@ function inIntegrationOf(graph, to, provider) {
|
|
|
214
233
|
return owner?.slot === INTEGRATION_SLOT && owner.bindings?.provider === provider;
|
|
215
234
|
}
|
|
216
235
|
|
|
236
|
+
/** The pseudo edges of one test consumer file: its import/export declarations that resolve to graph files it may reach. */
|
|
237
|
+
function consumerFileEdges({ ts, options, graph, config }, rel, provider) {
|
|
238
|
+
const abs = path.join(config.root, ...rel.split('/'));
|
|
239
|
+
let text;
|
|
240
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { return []; }
|
|
241
|
+
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
|
|
242
|
+
const edges = [];
|
|
243
|
+
for (const statement of sourceFile.statements) {
|
|
244
|
+
if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
|
|
245
|
+
const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, abs, options, ts.sys).resolvedModule?.resolvedFileName;
|
|
246
|
+
const to = resolved ? graph.abs(canonical(resolved)) : null;
|
|
247
|
+
if (to && (provider === null || inIntegrationOf(graph, to, provider))) edges.push({ from: rel, to, edge: { declaration: statement }, reexport: false });
|
|
248
|
+
}
|
|
249
|
+
return edges;
|
|
250
|
+
}
|
|
251
|
+
|
|
217
252
|
/**
|
|
218
253
|
* Pseudo edges (read from disk, outside the production program) to graph files: from the unit role specs, the fixture builders and
|
|
219
254
|
* the test world files to anything, and from an integration spec only to the integration of its own provider folder.
|
|
220
255
|
*/
|
|
221
256
|
function testConsumerEdges({ context, graph, config }) {
|
|
222
|
-
const { ts }
|
|
223
|
-
const options = context.projects?.[0]?.options ?? {};
|
|
224
|
-
const edges = [];
|
|
257
|
+
const resolution = { ts: context.ts, options: context.projects?.[0]?.options ?? {}, graph, config };
|
|
225
258
|
const consumers = [...treeOf(config.root).files]
|
|
226
259
|
.map(file => ({ file, any: testConsumer(graph, file) }))
|
|
227
260
|
.map(({ file, any }) => ({ file, provider: any ? null : integrationSpecProvider(graph, file), any }))
|
|
228
261
|
.filter(({ any, provider }) => any || provider !== null)
|
|
229
262
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
230
|
-
|
|
231
|
-
const abs = path.join(config.root, ...rel.split('/'));
|
|
232
|
-
let text;
|
|
233
|
-
try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
|
|
234
|
-
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
|
|
235
|
-
for (const statement of sourceFile.statements) {
|
|
236
|
-
if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
|
|
237
|
-
const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, abs, options, ts.sys).resolvedModule?.resolvedFileName;
|
|
238
|
-
const to = resolved ? graph.abs(canonical(resolved)) : null;
|
|
239
|
-
if (to && (provider === null || inIntegrationOf(graph, to, provider))) edges.push({ from: rel, to, edge: { declaration: statement }, reexport: false });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return edges;
|
|
263
|
+
return consumers.flatMap(({ file: rel, provider }) => consumerFileEdges(resolution, rel, provider));
|
|
243
264
|
}
|
|
244
265
|
|
|
245
|
-
|
|
246
|
-
const { ts } = context;
|
|
247
|
-
const violations = [];
|
|
266
|
+
const edgeIndex = edges => {
|
|
248
267
|
const edgesTo = new Map();
|
|
249
|
-
const
|
|
250
|
-
const known = graph.unit(rel);
|
|
251
|
-
if (known) return known;
|
|
252
|
-
const classified = graph.resolver.classifyPath(rel);
|
|
253
|
-
const owner = classified.slot ? graph.resolver.ownerOf(rel) : null;
|
|
254
|
-
return owner ? `${owner.slot}:${owner.root}` : null;
|
|
255
|
-
};
|
|
256
|
-
for (const edge of [...graph.edges, ...testConsumerEdges({ context, graph, config })]) {
|
|
268
|
+
for (const edge of edges) {
|
|
257
269
|
if (!edgesTo.has(edge.to)) edgesTo.set(edge.to, []);
|
|
258
270
|
edgesTo.get(edge.to).push(edge);
|
|
259
271
|
}
|
|
272
|
+
return edgesTo;
|
|
273
|
+
};
|
|
260
274
|
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return result;
|
|
284
|
-
};
|
|
275
|
+
const unitOfFile = (graph, rel) => {
|
|
276
|
+
const known = graph.unit(rel);
|
|
277
|
+
if (known) return known;
|
|
278
|
+
const classified = graph.resolver.classifyPath(rel);
|
|
279
|
+
const owner = classified.slot ? graph.resolver.ownerOf(rel) : null;
|
|
280
|
+
return owner ? `${owner.slot}:${owner.root}` : null;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const consumeReexportedName = (context, result, edge, pair, depth, visiting) => {
|
|
284
|
+
if (pair.exported === null) { result.names.add(pair.imported); return; }
|
|
285
|
+
// A re-export by name: the name is used when the re-exporting file's own export is used further on.
|
|
286
|
+
const reexporter = edge.from;
|
|
287
|
+
if (visiting.has(reexporter) || depth >= CHAIN_DEPTH) { result.names.add(pair.imported); return; }
|
|
288
|
+
if (!(context.edgesTo.get(reexporter)?.length)) {
|
|
289
|
+
// nobody imports the re-exporting file: a framework entry (route file) uses it, an unused owner entry does not
|
|
290
|
+
const holder = context.graph.files.get(reexporter);
|
|
291
|
+
if (!holder?.owner || context.graph.resolver.slot(holder.owner.slot)?.tier === 'app') result.names.add(pair.imported);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const next = consumedNames(context, reexporter, null, depth + 1, new Set([...visiting, reexporter]));
|
|
295
|
+
if (next.all || next.names.has(pair.exported)) result.names.add(pair.imported);
|
|
296
|
+
};
|
|
285
297
|
|
|
298
|
+
/** What the importers of `file` take from it; edges from inside `ownerKey` are skipped when given. */
|
|
299
|
+
const consumedNames = (context, file, ownerKey, depth, visiting) => {
|
|
300
|
+
const result = { all: false, names: new Set() };
|
|
301
|
+
for (const edge of context.edgesTo.get(file) ?? []) {
|
|
302
|
+
if (ownerKey && unitOfFile(context.graph, edge.from) === ownerKey) continue;
|
|
303
|
+
const bindings = bindingsOf(context.ts, edge.edge.declaration);
|
|
304
|
+
if (bindings.all) { result.all = true; continue; }
|
|
305
|
+
for (const pair of bindings.pairs) consumeReexportedName(context, result, edge, pair, depth, visiting);
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/** Adds to `used` the entry exports reached by a consumer outside the owner that imports a file the entry re-exports by name. */
|
|
311
|
+
const addReexportedUses = (context, key, publicEntry, used) => {
|
|
312
|
+
const { ts, graph } = context;
|
|
313
|
+
for (const edge of graph.edges) {
|
|
314
|
+
if (edge.from !== publicEntry || !edge.reexport || graph.unit(edge.to) !== key) continue;
|
|
315
|
+
const reexport = bindingsOf(ts, edge.edge.declaration);
|
|
316
|
+
const inner = consumedNames(context, edge.to, key, 0, new Set([publicEntry, edge.to]));
|
|
317
|
+
if (reexport.all) continue;
|
|
318
|
+
for (const pair of reexport.pairs) if (inner.all || inner.names.has(pair.imported)) used.names.add(pair.exported);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const unusedExportViolation = (publicEntry, name, line, owner) => ({
|
|
323
|
+
ruleId: 'HFS_UNUSED_EXPORT',
|
|
324
|
+
path: publicEntry, line, column: 1,
|
|
325
|
+
name, owner: owner.root, slot: owner.slot,
|
|
326
|
+
message: `${publicEntry} exports ${name}, but no production file outside ${owner.root || 'the repository root'} imports it; remove the export (only a unit role spec, a fixture builder, a test world file, or an integration spec of this integration's own provider folder counts besides production files).`,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
/** The unused-export violations and the export count of one public entry of an owner. */
|
|
330
|
+
const publicEntryExports = (context, key, owner, publicEntry) => {
|
|
331
|
+
const names = exportedNames(context.ts, context.graph.files.get(publicEntry).sourceFile);
|
|
332
|
+
const used = consumedNames(context, publicEntry, key, 0, new Set([publicEntry]));
|
|
333
|
+
addReexportedUses(context, key, publicEntry, used);
|
|
334
|
+
const violations = [...names]
|
|
335
|
+
.filter(([name]) => !(used.all || used.names.has(name)))
|
|
336
|
+
.map(([name, line]) => unusedExportViolation(publicEntry, name, line, owner));
|
|
337
|
+
return { violations, exports: names.size };
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const ownerExportFindings = (context, config) => {
|
|
341
|
+
const { graph } = context;
|
|
342
|
+
const violations = [];
|
|
286
343
|
let owners = 0;
|
|
287
344
|
let exports = 0;
|
|
288
345
|
for (const [key, owner] of graph.ownerRoots) {
|
|
@@ -291,30 +348,20 @@ export function checkDeadExports({ context, graph, config }) {
|
|
|
291
348
|
if (!entry) continue;
|
|
292
349
|
owners += 1;
|
|
293
350
|
for (const publicEntry of [entry, ...extraEntries(graph, entry)]) {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
for (const edge of graph.edges) {
|
|
298
|
-
if (edge.from !== publicEntry || !edge.reexport || graph.unit(edge.to) !== key) continue;
|
|
299
|
-
const reexport = bindingsOf(ts, edge.edge.declaration);
|
|
300
|
-
const inner = consumed(edge.to, key, 0, new Set([publicEntry, edge.to]));
|
|
301
|
-
if (reexport.all) continue;
|
|
302
|
-
for (const pair of reexport.pairs) if (inner.all || inner.names.has(pair.imported)) used.names.add(pair.exported);
|
|
303
|
-
}
|
|
304
|
-
for (const [name, line] of names) {
|
|
305
|
-
exports += 1;
|
|
306
|
-
if (used.all || used.names.has(name)) continue;
|
|
307
|
-
violations.push({
|
|
308
|
-
ruleId: 'HFS_UNUSED_EXPORT',
|
|
309
|
-
path: publicEntry, line, column: 1,
|
|
310
|
-
name, owner: owner.root, slot: owner.slot,
|
|
311
|
-
message: `${publicEntry} exports ${name}, but no production file outside ${owner.root || 'the repository root'} imports it; remove the export (only a unit role spec, a fixture builder, a test world file, or an integration spec of this integration's own provider folder counts besides production files).`,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
351
|
+
const found = publicEntryExports(context, key, owner, publicEntry);
|
|
352
|
+
exports += found.exports;
|
|
353
|
+
violations.push(...found.violations);
|
|
314
354
|
}
|
|
315
355
|
}
|
|
356
|
+
return { violations, owners, exports };
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export function checkDeadExports({ context, graph, config }) {
|
|
360
|
+
const edgesTo = edgeIndex([...graph.edges, ...testConsumerEdges({ context, graph, config })]);
|
|
361
|
+
const analysis = { ...context, graph, edgesTo };
|
|
362
|
+
const exports = ownerExportFindings(analysis, config);
|
|
316
363
|
const files = deadFiles(graph, config);
|
|
317
|
-
const
|
|
318
|
-
violations.push(...files.violations);
|
|
319
|
-
return { violations, coverage: { status: 'checked', owners
|
|
364
|
+
const dead = exports.violations.length;
|
|
365
|
+
exports.violations.push(...files.violations);
|
|
366
|
+
return { violations: exports.violations, coverage: { status: 'checked', owners: exports.owners, exports: exports.exports, dead, files: files.judged, roots: files.roots, unusedFiles: files.violations.length } };
|
|
320
367
|
}
|