@starci/eslint-canon-be 3.0.10 → 3.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 +7 -1
- package/README.md +2 -1
- package/builders.mjs +1 -1
- package/comments.mjs +1 -1
- package/config-owner.mjs +1 -1
- package/index.mjs +2 -0
- package/lib/config.mjs +4 -1
- package/lib/doors.mjs +3 -1
- package/module-shape.mjs +1 -2
- package/package.json +1 -1
- package/resilience.mjs +1 -1
- 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/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- 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/git/lib.mjs +34 -1
- 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/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/project-graph.mjs +4 -1
- 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/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- 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/trailing-slashes.mjs +8 -0
- 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/git.mjs +2 -2
- package/runtime/scripts/lib/i18n.mjs +3 -2
- 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/regex.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
- 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/schema-authority.mjs +1 -1
- package/service-deps.mjs +1 -1
- package/sonar-parity.mjs +19 -0
- package/suppression.mjs +2 -6
- package/test-world.mjs +2 -10
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { machineKit } from './machine-ast.mjs';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* R56 `hooks-are-hooks` (FE_HOOKS_ARE_HOOKS), the repository half of the eslint rule `hooks-folder-holds-hooks-only`, which
|
|
@@ -19,16 +20,9 @@ const HOOKS_SLOT = 'fe.hooks';
|
|
|
19
20
|
const HOOK_NAME = /^use[A-Z0-9]/u;
|
|
20
21
|
const SHARED = /\.shared\.[cm]?tsx?$/u;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const { config, graph } = input;
|
|
24
|
-
const kit = machineKit(input);
|
|
25
|
-
const { ts, resolver } = kit;
|
|
26
|
-
const violations = [];
|
|
27
|
-
const domains = new Set();
|
|
28
|
-
|
|
29
|
-
const tree = treeOf(config.root);
|
|
23
|
+
const sharedFilesOf = (tree, resolver, domains) => {
|
|
30
24
|
const shared = new Map(); // domain root -> [file]
|
|
31
|
-
for (const rel of [...tree.files].sort()) {
|
|
25
|
+
for (const rel of [...tree.files].sort(byCodeUnit)) {
|
|
32
26
|
const classified = resolver.classifyPath(rel);
|
|
33
27
|
if (classified.slot !== HOOKS_SLOT || !classified.root) continue;
|
|
34
28
|
domains.add(classified.root);
|
|
@@ -37,6 +31,10 @@ export function checkHooksAreHooks(input) {
|
|
|
37
31
|
if (!shared.has(classified.root)) shared.set(classified.root, []);
|
|
38
32
|
shared.get(classified.root).push({ rel, base });
|
|
39
33
|
}
|
|
34
|
+
return shared;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const reportSharedFileViolations = (shared, violations) => {
|
|
40
38
|
for (const [root, list] of shared) {
|
|
41
39
|
const domain = path.posix.basename(root);
|
|
42
40
|
const at = (item, message) => violations.push({ ruleId: RULE, path: item.rel, line: 1, column: 1, domain, message });
|
|
@@ -49,40 +47,62 @@ export function checkHooksAreHooks(input) {
|
|
|
49
47
|
at(item, `${item.rel} is a second shared file of hooks domain ${domain}; a domain has exactly one, ${domain}.shared.ts. Merge the helpers into it.`);
|
|
50
48
|
}
|
|
51
49
|
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const helperDeclarationsOf = (statement, ts) => {
|
|
53
|
+
const found = [];
|
|
54
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) found.push([statement.name.text, statement.name]);
|
|
55
|
+
if (ts.isVariableStatement(statement)) {
|
|
56
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
57
|
+
const init = declaration.initializer;
|
|
58
|
+
if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) found.push([declaration.name.text, declaration.name]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
};
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
const recordDeclaredHelper = (declared, root, name, file, node) => {
|
|
65
|
+
if (HOOK_NAME.test(name)) return;
|
|
66
|
+
if (!declared.has(root)) declared.set(root, new Map());
|
|
67
|
+
const byName = declared.get(root);
|
|
68
|
+
if (!byName.has(name)) byName.set(name, []);
|
|
69
|
+
byName.get(name).push({ file, node });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const declaredHelpersOf = (graph, resolver, ts) => {
|
|
54
73
|
const declared = new Map(); // domain root -> name -> [{file, node}]
|
|
55
74
|
for (const file of graph.files.values()) {
|
|
56
75
|
if (file.slot !== HOOKS_SLOT) continue;
|
|
57
76
|
const root = resolver.classifyPath(file.rel).root;
|
|
58
77
|
for (const statement of file.sourceFile.statements) {
|
|
59
|
-
const
|
|
60
|
-
if (ts.isFunctionDeclaration(statement) && statement.name) found.push([statement.name.text, statement.name]);
|
|
61
|
-
if (ts.isVariableStatement(statement)) {
|
|
62
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
63
|
-
const init = declaration.initializer;
|
|
64
|
-
if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) found.push([declaration.name.text, declaration.name]);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const [name, node] of found) {
|
|
68
|
-
if (HOOK_NAME.test(name)) continue;
|
|
69
|
-
if (!declared.has(root)) declared.set(root, new Map());
|
|
70
|
-
const byName = declared.get(root);
|
|
71
|
-
if (!byName.has(name)) byName.set(name, []);
|
|
72
|
-
byName.get(name).push({ file, node });
|
|
73
|
-
}
|
|
78
|
+
for (const [name, node] of helperDeclarationsOf(statement, ts)) recordDeclaredHelper(declared, root, name, file, node);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
81
|
+
return declared;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const reportDuplicateHelpers = (declared, violations, kit) => {
|
|
76
85
|
for (const [root, byName] of declared) {
|
|
77
86
|
const domain = path.posix.basename(root);
|
|
78
87
|
for (const [name, list] of byName) {
|
|
79
88
|
if (list.length < 2) continue;
|
|
80
89
|
for (const item of list.slice(1)) {
|
|
81
|
-
const others = list.map(entry => entry.file.rel).filter(rel => rel !== item.file.rel).sort();
|
|
90
|
+
const others = list.map(entry => entry.file.rel).filter(rel => rel !== item.file.rel).sort(byCodeUnit);
|
|
82
91
|
violations.push({ ruleId: RULE, path: item.file.rel, ...kit.at(item.file.rel, item.file.sourceFile, item.node), domain, helper: name,
|
|
83
92
|
message: `${name} is declared again in hooks domain ${domain} (also in ${others.join(', ')}); a helper the hooks share is written once, in ${domain}.shared.ts.` });
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function checkHooksAreHooks(input) {
|
|
99
|
+
const { config, graph } = input;
|
|
100
|
+
const kit = machineKit(input);
|
|
101
|
+
const { ts, resolver } = kit;
|
|
102
|
+
const violations = [];
|
|
103
|
+
const domains = new Set();
|
|
104
|
+
const shared = sharedFilesOf(treeOf(config.root), resolver, domains);
|
|
105
|
+
reportSharedFileViolations(shared, violations);
|
|
106
|
+
reportDuplicateHelpers(declaredHelpersOf(graph, resolver, ts), violations, kit);
|
|
87
107
|
return { violations, coverage: { status: 'checked', domains: domains.size } };
|
|
88
108
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R106 `i18n-keys` (FE_I18N_KEYS). The catalogs of an app (`apps/<app>/src/modules/i18n/messages/<locale>.json`, slot fe.modules.i18n) and
|
|
@@ -40,15 +41,8 @@ const held = (leaves, full) => leaves.has(full) || [...leaves].some(leaf => leaf
|
|
|
40
41
|
const tailMatches = (pattern, leafSegments) => pattern.some(segment => segment !== '*' && segment !== '') && pattern.length <= leafSegments.length
|
|
41
42
|
&& pattern.every((segment, index) => segment === '*' || segment === leafSegments[leafSegments.length - pattern.length + index]);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const violations = [];
|
|
46
|
-
const report = (file, node, message, extra = {}) => {
|
|
47
|
-
const at = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
|
|
48
|
-
violations.push({ ruleId: RULE, path: file.rel, line: at.line + 1, column: at.character + 1, message, ...extra });
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const catalogsByApp = new Map();
|
|
44
|
+
function catalogsPerApp(config, graph) {
|
|
45
|
+
const catalogs = new Map();
|
|
52
46
|
const seen = new Set();
|
|
53
47
|
for (const file of graph.files.values()) {
|
|
54
48
|
const app = APP_SOURCE.exec(file.rel)?.[1];
|
|
@@ -56,9 +50,113 @@ export function checkI18nKeys({ config, graph, context }) {
|
|
|
56
50
|
seen.add(app);
|
|
57
51
|
const dir = `apps/${app}/src/modules/i18n/messages/`;
|
|
58
52
|
let names = [];
|
|
59
|
-
try { names = fs.readdirSync(path.join(config.root, dir)).filter(name => name.endsWith('.json')).sort(); } catch { /* the app has no catalog directory: FE_I18N_PLACEMENT's finding */ }
|
|
60
|
-
if (names.length)
|
|
53
|
+
try { names = fs.readdirSync(path.join(config.root, dir)).filter(name => name.endsWith('.json')).sort(byCodeUnit); } catch { /* the app has no catalog directory: FE_I18N_PLACEMENT's finding */ }
|
|
54
|
+
if (names.length) catalogs.set(dir, names.map(name => ({ rel: `${dir}${name}`, locale: name.slice(0, -5) })));
|
|
55
|
+
}
|
|
56
|
+
return catalogs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function patternsOf(ts, files) {
|
|
60
|
+
const patterns = [];
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const visit = node => {
|
|
63
|
+
if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
|
|
64
|
+
else if (ts.isTemplateExpression(node)) patterns.push([node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').replaceAll('\u0000', '*').split('.'));
|
|
65
|
+
ts.forEachChild(node, visit);
|
|
66
|
+
};
|
|
67
|
+
visit(file.sourceFile);
|
|
68
|
+
}
|
|
69
|
+
return patterns;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function translationFactories(ts, file) {
|
|
73
|
+
const factories = new Set();
|
|
74
|
+
for (const statement of file.sourceFile.statements) {
|
|
75
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MODULES.has(statement.moduleSpecifier.text)) continue;
|
|
76
|
+
const named = statement.importClause?.namedBindings;
|
|
77
|
+
if (!named || !ts.isNamedImports(named)) continue;
|
|
78
|
+
for (const element of named.elements) {
|
|
79
|
+
if (!element.isTypeOnly && FACTORIES.has((element.propertyName ?? element.name).text)) factories.add(element.name.text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return factories;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function namespaceForCall(ts, file, init, factories) {
|
|
86
|
+
let expression = init;
|
|
87
|
+
if (ts.isAwaitExpression(expression)) expression = expression.expression;
|
|
88
|
+
if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || !factories.has(expression.expression.text)) return null;
|
|
89
|
+
const [first] = expression.arguments;
|
|
90
|
+
if (first === undefined) return '';
|
|
91
|
+
if (ts.isStringLiteralLike(first)) return first.text;
|
|
92
|
+
if (!ts.isObjectLiteralExpression(first)) return null;
|
|
93
|
+
const property = first.properties.find(candidate => ts.isPropertyAssignment(candidate) && candidate.name.getText(file.sourceFile) === 'namespace');
|
|
94
|
+
if (!property) return '';
|
|
95
|
+
return ts.isStringLiteralLike(property.initializer) ? property.initializer.text : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function translatorNamespaces(ts, file, factories) {
|
|
99
|
+
const namespaces = new Map();
|
|
100
|
+
const collect = node => {
|
|
101
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
102
|
+
const namespace = namespaceForCall(ts, file, node.initializer, factories);
|
|
103
|
+
if (namespace !== null) namespaces.set(node, namespace);
|
|
104
|
+
}
|
|
105
|
+
ts.forEachChild(node, collect);
|
|
106
|
+
};
|
|
107
|
+
collect(file.sourceFile);
|
|
108
|
+
return namespaces;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function keysOf(ts, argument) {
|
|
112
|
+
if (ts.isConditionalExpression(argument)) return [...keysOf(ts, argument.whenTrue), ...keysOf(ts, argument.whenFalse)];
|
|
113
|
+
if (ts.isParenthesizedExpression(argument) || ts.isAsExpression(argument) || ts.isSatisfiesExpression(argument)) return keysOf(ts, argument.expression);
|
|
114
|
+
return ts.isStringLiteralLike(argument) ? [argument] : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reportCallKeys(reads, node) {
|
|
118
|
+
const { ts, file, checker, namespaces, loaded, app, report } = reads;
|
|
119
|
+
const callee = node.expression;
|
|
120
|
+
let owner = ts.isIdentifier(callee) ? callee : null;
|
|
121
|
+
if (ts.isPropertyAccessExpression(callee) && READERS.has(callee.name.text) && ts.isIdentifier(callee.expression)) owner = callee.expression;
|
|
122
|
+
const [first] = node.arguments;
|
|
123
|
+
const declaration = owner ? checker.getSymbolAtLocation(owner)?.valueDeclaration : null;
|
|
124
|
+
if (!owner || first === undefined || !namespaces.has(declaration)) return;
|
|
125
|
+
for (const literal of keysOf(ts, first)) {
|
|
126
|
+
const namespace = namespaces.get(declaration);
|
|
127
|
+
const full = namespace ? `${namespace}.${literal.text}` : literal.text;
|
|
128
|
+
const lacking = loaded.filter(catalog => !held(catalog.leaves, full)).map(catalog => catalog.locale);
|
|
129
|
+
if (lacking.length) report(file, literal, `${file.rel} reads "${full}" and ${lacking.join(', ')} ${lacking.length === 1 ? 'holds' : 'hold'} no such key; add it to every catalog of ${app} or read a key that exists`, { key: full, locales: lacking });
|
|
61
130
|
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reportTranslationReads(ts, file, checker, namespaces, loaded, app, report) {
|
|
134
|
+
const reads = { ts, file, checker, namespaces, loaded, app, report };
|
|
135
|
+
const read = node => {
|
|
136
|
+
if (ts.isCallExpression(node)) reportCallKeys(reads, node);
|
|
137
|
+
ts.forEachChild(node, read);
|
|
138
|
+
};
|
|
139
|
+
read(file.sourceFile);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function reportUnusedCatalogKeys(primary, loaded, patterns, app, violations) {
|
|
143
|
+
const every = new Set(loaded.flatMap(catalog => [...catalog.leaves]));
|
|
144
|
+
for (const leaf of [...every].sort(byCodeUnit)) {
|
|
145
|
+
const segments = leaf.split('.');
|
|
146
|
+
if (patterns.some(pattern => tailMatches(pattern, segments))) continue;
|
|
147
|
+
violations.push({ ruleId: RULE, path: primary.rel, line: 1, column: 1, message: `${primary.rel} holds the key "${leaf}" and no source of ${app} or the shared packages reads it; delete the key from every catalog`, key: leaf });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function checkI18nKeys({ config, graph, context }) {
|
|
152
|
+
const ts = context.ts;
|
|
153
|
+
const violations = [];
|
|
154
|
+
const report = (file, node, message, extra = {}) => {
|
|
155
|
+
const at = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
|
|
156
|
+
violations.push({ ruleId: RULE, path: file.rel, line: at.line + 1, column: at.character + 1, message, ...extra });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const catalogsByApp = catalogsPerApp(config, graph);
|
|
62
160
|
|
|
63
161
|
let apps = 0;
|
|
64
162
|
for (const [dir, catalogs] of catalogsByApp) {
|
|
@@ -68,79 +166,19 @@ export function checkI18nKeys({ config, graph, context }) {
|
|
|
68
166
|
apps += 1;
|
|
69
167
|
const own = [...graph.files.values()].filter(file => file.rel.startsWith(`apps/${app}/src/`));
|
|
70
168
|
const shared = [...graph.files.values()].filter(file => /^packages\/[^/]+\/src\//u.test(file.rel));
|
|
71
|
-
const patterns = [];
|
|
72
|
-
|
|
73
|
-
for (const file of [...own, ...shared]) {
|
|
74
|
-
const visit = node => {
|
|
75
|
-
if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
|
|
76
|
-
else if (ts.isTemplateExpression(node)) patterns.push([node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').split('\u0000').join('*').split('.'));
|
|
77
|
-
ts.forEachChild(node, visit);
|
|
78
|
-
};
|
|
79
|
-
visit(file.sourceFile);
|
|
80
|
-
}
|
|
169
|
+
const patterns = patternsOf(ts, [...own, ...shared]);
|
|
81
170
|
|
|
82
171
|
for (const file of own) {
|
|
83
|
-
const factories =
|
|
84
|
-
for (const statement of file.sourceFile.statements) {
|
|
85
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MODULES.has(statement.moduleSpecifier.text)) continue;
|
|
86
|
-
const named = statement.importClause?.namedBindings;
|
|
87
|
-
if (!named || !ts.isNamedImports(named)) continue;
|
|
88
|
-
for (const element of named.elements) if (!element.isTypeOnly && FACTORIES.has((element.propertyName ?? element.name).text)) factories.add(element.name.text);
|
|
89
|
-
}
|
|
172
|
+
const factories = translationFactories(ts, file);
|
|
90
173
|
if (!factories.size) continue;
|
|
91
174
|
const checker = context.checkerFor(file.sourceFile.fileName);
|
|
92
|
-
const namespaces =
|
|
93
|
-
const collect = node => {
|
|
94
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
95
|
-
let init = node.initializer;
|
|
96
|
-
if (ts.isAwaitExpression(init)) init = init.expression;
|
|
97
|
-
if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && factories.has(init.expression.text)) {
|
|
98
|
-
const [first] = init.arguments;
|
|
99
|
-
if (first === undefined) namespaces.set(node, '');
|
|
100
|
-
else if (ts.isStringLiteralLike(first)) namespaces.set(node, first.text);
|
|
101
|
-
else if (ts.isObjectLiteralExpression(first)) {
|
|
102
|
-
const property = first.properties.find(candidate => ts.isPropertyAssignment(candidate) && candidate.name.getText(file.sourceFile) === 'namespace');
|
|
103
|
-
if (!property) namespaces.set(node, '');
|
|
104
|
-
else if (ts.isStringLiteralLike(property.initializer)) namespaces.set(node, property.initializer.text);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
ts.forEachChild(node, collect);
|
|
109
|
-
};
|
|
110
|
-
collect(file.sourceFile);
|
|
175
|
+
const namespaces = translatorNamespaces(ts, file, factories);
|
|
111
176
|
if (!namespaces.size) continue;
|
|
112
|
-
|
|
113
|
-
if (ts.isConditionalExpression(argument)) return [...keysOf(argument.whenTrue), ...keysOf(argument.whenFalse)];
|
|
114
|
-
if (ts.isParenthesizedExpression(argument) || ts.isAsExpression(argument) || ts.isSatisfiesExpression(argument)) return keysOf(argument.expression);
|
|
115
|
-
return ts.isStringLiteralLike(argument) ? [argument] : [];
|
|
116
|
-
};
|
|
117
|
-
const read = node => {
|
|
118
|
-
if (ts.isCallExpression(node)) {
|
|
119
|
-
const callee = node.expression;
|
|
120
|
-
const owner = ts.isIdentifier(callee) ? callee : (ts.isPropertyAccessExpression(callee) && READERS.has(callee.name.text) && ts.isIdentifier(callee.expression) ? callee.expression : null);
|
|
121
|
-
const [first] = node.arguments;
|
|
122
|
-
const declaration = owner ? checker.getSymbolAtLocation(owner)?.valueDeclaration : null;
|
|
123
|
-
if (owner && first !== undefined && namespaces.has(declaration)) {
|
|
124
|
-
for (const literal of keysOf(first)) {
|
|
125
|
-
const namespace = namespaces.get(declaration);
|
|
126
|
-
const full = namespace ? `${namespace}.${literal.text}` : literal.text;
|
|
127
|
-
const lacking = loaded.filter(catalog => !held(catalog.leaves, full)).map(catalog => catalog.locale);
|
|
128
|
-
if (lacking.length) report(file, literal, `${file.rel} reads "${full}" and ${lacking.join(', ')} ${lacking.length === 1 ? 'holds' : 'hold'} no such key; add it to every catalog of ${app} or read a key that exists`, { key: full, locales: lacking });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
ts.forEachChild(node, read);
|
|
133
|
-
};
|
|
134
|
-
read(file.sourceFile);
|
|
177
|
+
reportTranslationReads(ts, file, checker, namespaces, loaded, app, report);
|
|
135
178
|
}
|
|
136
179
|
|
|
137
180
|
const [primary] = [...loaded].sort((a, b) => a.locale.localeCompare(b.locale));
|
|
138
|
-
|
|
139
|
-
for (const leaf of [...every].sort()) {
|
|
140
|
-
const segments = leaf.split('.');
|
|
141
|
-
if (patterns.some(pattern => tailMatches(pattern, segments))) continue;
|
|
142
|
-
violations.push({ ruleId: RULE, path: primary.rel, line: 1, column: 1, message: `${primary.rel} holds the key "${leaf}" and no source of ${app} or the shared packages reads it; delete the key from every catalog`, key: leaf });
|
|
143
|
-
}
|
|
181
|
+
reportUnusedCatalogKeys(primary, loaded, patterns, app, violations);
|
|
144
182
|
}
|
|
145
183
|
return { violations, coverage: { status: 'checked', apps } };
|
|
146
184
|
}
|