@starci/eslint-grammar-core 0.15.34 → 0.16.2
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 +14 -0
- package/README.md +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/why.js +15 -0
- package/dist/mascot.d.ts +28 -0
- package/dist/mascot.js +166 -0
- package/dist/surface-depth.d.ts +7 -0
- package/dist/surface-depth.js +92 -0
- package/docs/mascot.md +60 -0
- package/docs/surface-depth.md +40 -0
- package/package.json +2 -2
- package/runtime/bundle.json +74 -0
- package/runtime/engine/by-code-unit.mjs +6 -6
- package/runtime/engine/model-config.mjs +98 -76
- package/runtime/engine/orca-config.mjs +22 -17
- package/runtime/engine/release-config.mjs +22 -18
- package/runtime/engine/removed-vocabulary.mjs +21 -20
- package/runtime/engine/resources-config.mjs +42 -35
- package/runtime/engine/runtime-root.mjs +11 -13
- package/runtime/engine/secrets.mjs +170 -102
- package/runtime/engine/sonar-config.mjs +16 -13
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/rules.yaml +95 -6
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
- package/runtime/scripts/hfs/allows.mjs +37 -38
- package/runtime/scripts/hfs/architecture/config.mjs +302 -268
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
- package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
- package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
- package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
- package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
- package/runtime/scripts/hfs/edition-slots.mjs +43 -40
- package/runtime/scripts/hfs/param-names.mjs +6 -11
- package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
- package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
- package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
- package/runtime/scripts/hfs/slot-classify.mjs +42 -38
- package/runtime/scripts/hfs/slot-errors.mjs +8 -7
- package/runtime/scripts/hfs/slot-imports.mjs +70 -63
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
- package/runtime/scripts/hfs/slot-match.mjs +122 -111
- package/runtime/scripts/hfs/slot-path.mjs +0 -2
- package/runtime/scripts/hfs/slot-required.mjs +65 -59
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
- package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
- package/runtime/scripts/hfs/slots.mjs +175 -152
- package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
- package/runtime/scripts/hfs/view.mjs +38 -40
- package/runtime/scripts/lib/glob.mjs +23 -14
- package/runtime/scripts/lib/grammar-lint.mjs +33 -37
- package/runtime/scripts/lib/i18n.mjs +32 -35
- package/runtime/scripts/lib/json-clone.mjs +45 -36
- package/runtime/scripts/lib/json.mjs +26 -14
- package/runtime/scripts/lib/list.mjs +9 -14
- package/runtime/scripts/lib/path-key.mjs +53 -38
- package/runtime/scripts/lib/read-text.mjs +30 -17
- package/runtime/scripts/lib/regex.mjs +1 -2
- package/runtime/scripts/lib/source-phrases.mjs +13 -13
- package/runtime/scripts/lib/ts-analysis.mjs +28 -26
- package/runtime/scripts/lib/ts-ast.mjs +36 -42
- package/runtime/scripts/lib/walk.mjs +33 -27
- package/runtime/scripts/lib/yaml-cached.mjs +4 -4
|
@@ -4,13 +4,10 @@
|
|
|
4
4
|
// (db/types/, sides.<side>.reads of the slot manifest) is the one thing both sides do import: the generated database types.
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { isInside, slash } from './config.mjs';
|
|
7
|
-
|
|
8
7
|
const OTHER_SIDE = /^(be|fe)\//;
|
|
9
|
-
|
|
10
8
|
/** True when `target` (an absolute path) lies in a tree of the app root that the side declares it reads (`reads` of the side declaration). */
|
|
11
9
|
const readsAppTree = (config, target) => (config.hfs?.repo?.reads ?? [])
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
.some((read) => !OTHER_SIDE.test(read) && isInside(path.join(config.packageRoot, read), target));
|
|
14
11
|
/** Whether an import of `target` leaves the side the machine judges (config.root) for the app (config.packageRoot), declared app-root reads excepted. */
|
|
15
12
|
export const crossesSide = (config, target) => config.packageRoot !== undefined && config.packageRoot !== config.root
|
|
16
|
-
|
|
13
|
+
&& isInside(config.packageRoot, target) && !isInside(config.root, target) && !slash(target).includes('/node_modules/') && !readsAppTree(config, target);
|
|
@@ -1,300 +1,305 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
4
3
|
function appendProjectReferences(relative, parsed, config, ts, helpers, queue, errors) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
for (const reference of parsed.projectReferences ?? []) {
|
|
5
|
+
const referencedFile = helpers.typeScriptProjectReferencePath(ts, reference);
|
|
6
|
+
const absoluteReference = path.resolve(referencedFile);
|
|
7
|
+
if (!helpers.isInside(config.root, absoluteReference)) {
|
|
8
|
+
errors.push({ ruleId: 'ARCH_TSCONFIG_REFERENCE_OUTSIDE', project: relative, message: `Project reference leaves the repository: ${helpers.slash(path.relative(config.root, absoluteReference))}.` });
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
queue.push(helpers.slash(path.relative(config.root, absoluteReference)));
|
|
12
|
+
}
|
|
12
13
|
}
|
|
13
|
-
}
|
|
14
14
|
}
|
|
15
|
-
|
|
16
15
|
function readProject(relative, config, ts, helpers, state) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
16
|
+
const configFile = path.join(config.root, ...relative.split('/'));
|
|
17
|
+
if (!fs.existsSync(configFile)) {
|
|
18
|
+
state.errors.push({ ruleId: 'ARCH_TSCONFIG_MISSING', project: relative, message: `${relative} does not exist.` });
|
|
19
|
+
state.invalidProjects.add(relative);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { read, parsed } = helpers.readTypeScriptProject(ts, configFile);
|
|
23
|
+
if (parsed === null) {
|
|
24
|
+
state.errors.push(helpers.compilerError(ts, config.root, read.error, relative));
|
|
25
|
+
state.invalidProjects.add(relative);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (parsed.errors.length) {
|
|
29
|
+
state.errors.push(...parsed.errors.map(item => helpers.compilerError(ts, config.root, item, relative)));
|
|
30
|
+
state.invalidProjects.add(relative);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
appendProjectReferences(relative, parsed, config, ts, helpers, state.queue, state.errors);
|
|
34
|
+
state.parsedProjects.set(relative, parsed);
|
|
36
35
|
}
|
|
37
|
-
|
|
38
36
|
function loadProjectConfigurations(config, ts, helpers, errors) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
const state = { errors, parsedProjects: new Map(), invalidProjects: new Set(), queue: [...config.projects] };
|
|
38
|
+
const seenProjects = new Set();
|
|
39
|
+
while (state.queue.length) {
|
|
40
|
+
const relative = state.queue.shift(); // length checked by the loop condition
|
|
41
|
+
if (seenProjects.has(relative))
|
|
42
|
+
continue;
|
|
43
|
+
seenProjects.add(relative);
|
|
44
|
+
readProject(relative, config, ts, helpers, state);
|
|
45
|
+
}
|
|
46
|
+
return state;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
48
|
function pathMatches(config, paths, file, helpers) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const relative = helpers.slash(path.relative(config.root, file));
|
|
50
|
+
const normalized = helpers.slash(paths).replace(/\/$/, '');
|
|
51
|
+
return helpers.sameOrUnder(relative, normalized);
|
|
54
52
|
}
|
|
55
|
-
|
|
56
53
|
function visitProjectReferences(relative, selected, parsedProjects, config, ts, helpers) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
if (selected.has(relative))
|
|
55
|
+
return;
|
|
56
|
+
selected.add(relative);
|
|
57
|
+
for (const reference of parsedProjects.get(relative)?.projectReferences ?? []) {
|
|
58
|
+
const referencedFile = helpers.typeScriptProjectReferencePath(ts, reference);
|
|
59
|
+
const referenced = helpers.slash(path.relative(config.root, path.resolve(referencedFile)));
|
|
60
|
+
if (parsedProjects.has(referenced))
|
|
61
|
+
visitProjectReferences(referenced, selected, parsedProjects, config, ts, helpers);
|
|
62
|
+
}
|
|
64
63
|
}
|
|
65
|
-
|
|
66
64
|
function invalidProjectMatches(config, paths, relative, helpers) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
const directory = path.dirname(path.join(config.root, ...relative.split('/')));
|
|
66
|
+
return paths.some(prefix => {
|
|
67
|
+
const absolute = path.join(config.root, ...helpers.slash(prefix).split('/'));
|
|
68
|
+
return helpers.isInside(directory, absolute);
|
|
69
|
+
});
|
|
72
70
|
}
|
|
73
|
-
|
|
74
71
|
function selectProjects(config, paths, ts, state, helpers) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
const selected = new Set();
|
|
73
|
+
const direct = new Set();
|
|
74
|
+
if (!paths.length)
|
|
75
|
+
return { selected, direct };
|
|
76
|
+
for (const [relative, parsed] of state.parsedProjects) {
|
|
77
|
+
if (parsed.fileNames.some(file => paths.some(prefix => pathMatches(config, prefix, file, helpers)))) {
|
|
78
|
+
direct.add(relative);
|
|
79
|
+
visitProjectReferences(relative, selected, state.parsedProjects, config, ts, helpers);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const relative of state.invalidProjects) {
|
|
83
|
+
if (invalidProjectMatches(config, paths, relative, helpers))
|
|
84
|
+
selected.add(relative);
|
|
82
85
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (invalidProjectMatches(config, paths, relative, helpers)) selected.add(relative);
|
|
86
|
-
}
|
|
87
|
-
state.errors.splice(0, state.errors.length, ...state.errors.filter(error => !error.project || selected.has(error.project)));
|
|
88
|
-
return { selected, direct };
|
|
86
|
+
state.errors.splice(0, state.errors.length, ...state.errors.filter(error => !error.project || selected.has(error.project)));
|
|
87
|
+
return { selected, direct };
|
|
89
88
|
}
|
|
90
|
-
|
|
91
89
|
function createProjects(config, paths, state, selection, ts, helpers) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
const projects = [];
|
|
91
|
+
for (const [relative, parsed] of state.parsedProjects) {
|
|
92
|
+
if (paths.length && !selection.selected.has(relative))
|
|
93
|
+
continue;
|
|
94
|
+
const rootNames = paths.length && selection.direct.has(relative)
|
|
95
|
+
? parsed.fileNames.filter(file => paths.some(prefix => pathMatches(config, prefix, file, helpers)))
|
|
96
|
+
: parsed.fileNames;
|
|
97
|
+
const program = helpers.createTypeScriptProgram(ts, { rootNames, options: parsed.options, projectReferences: parsed.projectReferences });
|
|
98
|
+
state.errors.push(...program.getSyntacticDiagnostics().filter(item => !item.file || helpers.isInside(config.root, item.file.fileName))
|
|
99
|
+
.map(item => helpers.compilerError(ts, config.root, item, relative, 'ARCH_SYNTAX_INVALID')));
|
|
100
|
+
projects.push({ relative, program, options: parsed.options });
|
|
101
|
+
}
|
|
102
|
+
return projects;
|
|
104
103
|
}
|
|
105
|
-
|
|
106
104
|
function ownershipRank(config, project, file, helpers) {
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
return helpers.isInside(path.dirname(path.join(config.root, ...project.relative.split('/'))), file)
|
|
106
|
+
? project.relative.split('/').length : -1;
|
|
109
107
|
}
|
|
110
|
-
|
|
111
108
|
function collectProjectFiles(config, projects, helpers) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
109
|
+
const fileMap = new Map();
|
|
110
|
+
const ownerByFile = new Map();
|
|
111
|
+
// Retain only the winning source occurrence while visiting each project. Equal ranks keep the first project,
|
|
112
|
+
// as does a file outside every project directory; the deepest enclosing tsconfig otherwise owns its checker.
|
|
113
|
+
// Within that project the last realpath alias supplies the source, matching the compiler's occurrence order.
|
|
114
|
+
for (const project of projects) {
|
|
115
|
+
for (const sourceFile of project.program.getSourceFiles()) {
|
|
116
|
+
if (!helpers.isProductionSource(config.root, sourceFile))
|
|
117
|
+
continue;
|
|
118
|
+
const name = helpers.canonical(sourceFile.fileName);
|
|
119
|
+
const owner = ownerByFile.get(name);
|
|
120
|
+
if (owner === project) {
|
|
121
|
+
fileMap.set(name, sourceFile);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (owner && ownershipRank(config, owner, name, helpers) >= ownershipRank(config, project, name, helpers))
|
|
125
|
+
continue;
|
|
126
|
+
ownerByFile.set(name, project);
|
|
127
|
+
fileMap.set(name, sourceFile);
|
|
128
|
+
}
|
|
126
129
|
}
|
|
127
|
-
|
|
128
|
-
return { fileMap, ownerByFile };
|
|
130
|
+
return { fileMap, ownerByFile };
|
|
129
131
|
}
|
|
130
|
-
|
|
131
132
|
function makeFileGraph(config, projects, helpers) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
const { fileMap, ownerByFile } = collectProjectFiles(config, projects, helpers);
|
|
134
|
+
const checkerByFile = new Map();
|
|
135
|
+
for (const [name, owner] of ownerByFile)
|
|
136
|
+
checkerByFile.set(name, owner.program.getTypeChecker());
|
|
137
|
+
const files = [...fileMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, file]) => file);
|
|
138
|
+
const edges = new Map([...fileMap.keys()].map((file) => [file, []]));
|
|
139
|
+
for (const [file, sourceFile] of fileMap) {
|
|
140
|
+
const sourceName = path.resolve(sourceFile.fileName);
|
|
141
|
+
if (sourceName !== file)
|
|
142
|
+
edges.set(sourceName, edges.get(file) ?? []);
|
|
143
|
+
}
|
|
144
|
+
return { fileMap, checkerByFile, ownerByFile, files, edges };
|
|
142
145
|
}
|
|
143
|
-
|
|
144
146
|
// Resolution entries contain failed lookup paths as well as hits. Keep only one bounded batch for the current project;
|
|
145
147
|
// graph edges retain the resolved targets, so rotating this scratch cache changes neither coverage nor findings.
|
|
146
148
|
const RESOLUTION_BATCH_SIZE = 1024;
|
|
147
|
-
|
|
148
149
|
function resolutionCache(state, project) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return state.resolutionCache;
|
|
150
|
+
if (state.resolutionProject !== project || state.resolutionCount === RESOLUTION_BATCH_SIZE) {
|
|
151
|
+
state.resolutionProject = project;
|
|
152
|
+
state.resolutionCount = 0;
|
|
153
|
+
state.resolutionCache = state.ts.createModuleResolutionCache(state.config.root, file => state.ts.sys.useCaseSensitiveFileNames ? file : file.toLowerCase(), project.options);
|
|
154
|
+
}
|
|
155
|
+
state.resolutionCount += 1;
|
|
156
|
+
return state.resolutionCache;
|
|
157
157
|
}
|
|
158
|
-
|
|
159
158
|
function addUnprovenFindings(sourceFile, project, references, errors, config, helpers) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
for (const item of references.unproven)
|
|
160
|
+
errors.push({
|
|
161
|
+
ruleId: 'ARCH_DYNAMIC_DEPENDENCY_UNPROVEN',
|
|
162
|
+
project: project.relative,
|
|
163
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
164
|
+
...helpers.sourceLocation(sourceFile, item.node),
|
|
165
|
+
message: `${item.kind} must use a string-literal module name so architecture coverage can resolve its dependency.`,
|
|
166
|
+
});
|
|
167
167
|
}
|
|
168
|
-
|
|
169
168
|
function unresolvedReferenceFinding(reference, sourceFile, project, config, workspaceNames, errors, helpers) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
169
|
+
const codeLike = !helpers.assetExtension.test(reference.specifier);
|
|
170
|
+
const workspaceImport = [...workspaceNames].some(name => helpers.sameOrUnder(reference.specifier, name));
|
|
171
|
+
const internal = reference.specifier.startsWith('.') || helpers.pathAliasMatches(reference.specifier, project.options.paths) || workspaceImport;
|
|
172
|
+
// A __generated__/ folder is the gitignored output of the app's codegen step (the GraphQL wire types, fe-stack codegen): it
|
|
173
|
+
// exists after `npm run codegen`, which lint, typecheck and build run first, and the compiler proves those imports there.
|
|
174
|
+
const generated = /(?:^|\/)__generated__(?:\/|$)/.test(reference.specifier);
|
|
175
|
+
if (codeLike && internal && !generated)
|
|
176
|
+
errors.push({
|
|
177
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_UNRESOLVED',
|
|
178
|
+
project: project.relative,
|
|
179
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
180
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
181
|
+
specifier: reference.specifier,
|
|
182
|
+
message: `Internal import ${reference.specifier} is not resolvable with ${project.relative}.`,
|
|
183
|
+
});
|
|
184
184
|
}
|
|
185
|
-
|
|
186
185
|
function internalReference(reference, project, workspaceNames, helpers) {
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
const workspaceImport = [...workspaceNames].some(name => reference.specifier === name || reference.specifier.startsWith(`${name}/`));
|
|
187
|
+
return reference.specifier.startsWith('.') || helpers.pathAliasMatches(reference.specifier, project.options.paths) || workspaceImport;
|
|
189
188
|
}
|
|
190
|
-
|
|
191
189
|
function appendBoundaryFinding(state, reference, sourceFile, project, actualTarget, { internal, reviewable }) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
190
|
+
const { config, errors, helpers } = state;
|
|
191
|
+
if (internal && helpers.crossesSide(config, actualTarget)) {
|
|
192
|
+
errors.push({
|
|
193
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_OUTSIDE',
|
|
194
|
+
project: project.relative,
|
|
195
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
196
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
197
|
+
specifier: reference.specifier,
|
|
198
|
+
message: `Internal import ${reference.specifier} leaves the ${path.basename(config.root)} side for ${helpers.slash(path.relative(config.packageRoot, actualTarget))}; nothing crosses the sides of an app.`,
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (internal && !reviewable) {
|
|
203
|
+
errors.push({
|
|
204
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_OUTSIDE',
|
|
205
|
+
project: project.relative,
|
|
206
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
207
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
208
|
+
specifier: reference.specifier,
|
|
209
|
+
message: `Internal import ${reference.specifier} resolves outside the checked repository.`,
|
|
210
|
+
});
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
216
214
|
}
|
|
217
|
-
|
|
218
215
|
function appendEdge(from, sourceFile, project, owningWorkspace, reference, actualTarget, state) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
216
|
+
if (!state.fileMap.has(actualTarget))
|
|
217
|
+
return;
|
|
218
|
+
const { config, ts, edges, edgeKeys, errors, helpers, workspaces } = state;
|
|
219
|
+
const edge = {
|
|
220
|
+
from,
|
|
221
|
+
to: actualTarget,
|
|
222
|
+
runtime: reference.runtime,
|
|
223
|
+
specifier: reference.specifier,
|
|
224
|
+
node: reference.node,
|
|
225
|
+
declaration: reference.declaration,
|
|
226
|
+
reexport: ts.isExportDeclaration(reference.declaration),
|
|
227
|
+
sourceFile,
|
|
228
|
+
project: project.relative,
|
|
229
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
230
|
+
};
|
|
231
|
+
const key = `${from}\0${actualTarget}\0${reference.node.getStart(sourceFile)}\0${reference.specifier}`;
|
|
232
|
+
if (!edgeKeys.has(key)) {
|
|
233
|
+
edges.get(from)?.push(edge);
|
|
234
|
+
edgeKeys.add(key);
|
|
235
|
+
}
|
|
236
|
+
const targetWorkspace = helpers.workspaceOf(workspaces, actualTarget);
|
|
237
|
+
if (owningWorkspace && targetWorkspace && owningWorkspace !== targetWorkspace) {
|
|
238
|
+
if (!owningWorkspace.app && targetWorkspace.app)
|
|
239
|
+
errors.push(helpers.boundaryViolation(config.root, edge, owningWorkspace, targetWorkspace, 'ARCH_PACKAGE_IMPORTS_APP', 'A reusable workspace package cannot depend on an application workspace.'));
|
|
240
|
+
if (!helpers.packageExported(ts, targetWorkspace, reference.specifier, actualTarget))
|
|
241
|
+
errors.push(helpers.boundaryViolation(config.root, edge, owningWorkspace, targetWorkspace, 'ARCH_PACKAGE_EXPORT_BYPASS', 'Cross-package imports must use the target package name and a declared package export.'));
|
|
242
|
+
}
|
|
240
243
|
}
|
|
241
|
-
|
|
242
244
|
function processReference(from, sourceFile, project, owningWorkspace, reference, state) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
245
|
+
const { config, ts, workspaceNames, workspaces, host, errors, helpers } = state;
|
|
246
|
+
// A workspace package is read at its source, never at its build: the same import resolves whether or not dist exists.
|
|
247
|
+
const workspaceTarget = workspaces.filter(item => item.root !== helpers.canonical(config.root)).map(item => helpers.workspaceSourceEntry(ts, item, reference.specifier)).find(Boolean);
|
|
248
|
+
const resolvedName = workspaceTarget ?? helpers.resolveTypeScriptModule(ts, reference.specifier, sourceFile.fileName, project.options, host, resolutionCache(state, project));
|
|
249
|
+
if (!resolvedName) {
|
|
250
|
+
unresolvedReferenceFinding(reference, sourceFile, project, config, workspaceNames, errors, helpers);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const actualTarget = helpers.canonical(resolvedName);
|
|
254
|
+
const internal = internalReference(reference, project, workspaceNames, helpers);
|
|
255
|
+
// The boundary is the checkout, not the checked project: a path alias that lands in a sibling
|
|
256
|
+
// package of the same repository (`@fe-kit/*` -> packages/fe-kit) is still source a reviewer can
|
|
257
|
+
// open, while anything past the repository, or anything inside an installed dependency tree, is
|
|
258
|
+
// not. `config.repository` is null when there is no git checkout around the project, and the
|
|
259
|
+
// project is then the boundary it always was.
|
|
260
|
+
const reviewable = helpers.isInside(config.root, actualTarget)
|
|
261
|
+
|| Boolean(config.repository && helpers.isInside(config.repository, actualTarget) && !helpers.slash(actualTarget).includes('/node_modules/'));
|
|
262
|
+
// A side of an app (config.root is be/ or fe/) imports nothing of the app outside itself: the root holds no source, the other
|
|
263
|
+
// side is another program, and a declared read (sides.fe.reads, be/contracts/) is codegen input, never an import.
|
|
264
|
+
if (appendBoundaryFinding(state, reference, sourceFile, project, actualTarget, { internal, reviewable }))
|
|
265
|
+
return;
|
|
266
|
+
appendEdge(from, sourceFile, project, owningWorkspace, reference, actualTarget, state);
|
|
264
267
|
}
|
|
265
|
-
|
|
266
268
|
function processSourceFile(from, sourceFile, state) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
const { config, ts, workspaces, ownerByFile, helpers } = state;
|
|
270
|
+
const owningWorkspace = helpers.workspaceOf(workspaces, from);
|
|
271
|
+
const project = ownerByFile.get(from);
|
|
272
|
+
if (!project)
|
|
273
|
+
return;
|
|
274
|
+
const references = helpers.moduleReferences(ts, sourceFile, project.program.getTypeChecker());
|
|
275
|
+
addUnprovenFindings(sourceFile, project, references, state.errors, config, helpers);
|
|
276
|
+
for (const reference of references.found)
|
|
277
|
+
processReference(from, sourceFile, project, owningWorkspace, reference, state);
|
|
274
278
|
}
|
|
275
|
-
|
|
276
279
|
function dependencyGraph(config, loaded, fileGraph, ownerByFile, errors, helpers) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
const { ts } = loaded;
|
|
281
|
+
const edgeKeys = new Set();
|
|
282
|
+
const host = { ...ts.sys, fileExists: ts.sys.fileExists, readFile: ts.sys.readFile, realpath: ts.sys.realpath };
|
|
283
|
+
const workspaces = helpers.workspaceMetadata(config);
|
|
284
|
+
const workspaceNames = new Set(workspaces.map(item => item.name).filter((name) => Boolean(name)));
|
|
285
|
+
const state = { config, ts, fileMap: fileGraph.fileMap, edges: fileGraph.edges, edgeKeys, errors, workspaces, workspaceNames, host, ownerByFile, helpers, resolutionCount: 0 };
|
|
286
|
+
for (const [from, sourceFile] of fileGraph.fileMap)
|
|
287
|
+
processSourceFile(from, sourceFile, state);
|
|
288
|
+
return { edges: fileGraph.edges, workspaces };
|
|
285
289
|
}
|
|
286
|
-
|
|
287
290
|
export function contextFromProjects(config, loaded, paths, helpers) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
291
|
+
const { ts } = loaded;
|
|
292
|
+
const errors = [];
|
|
293
|
+
const projectState = loadProjectConfigurations(config, ts, helpers, errors);
|
|
294
|
+
const selection = selectProjects(config, paths, ts, projectState, helpers);
|
|
295
|
+
const projects = createProjects(config, paths, projectState, selection, ts, helpers);
|
|
296
|
+
const fileGraph = makeFileGraph(config, projects, helpers);
|
|
297
|
+
const { edges, workspaces } = dependencyGraph(config, loaded, fileGraph, fileGraph.ownerByFile, errors, helpers);
|
|
298
|
+
if (projects.length && fileGraph.files.length === 0)
|
|
299
|
+
errors.push({ ruleId: 'ARCH_NO_SOURCE', message: 'The configured TypeScript projects contain no production TypeScript or JavaScript source.' });
|
|
300
|
+
const { checkerByFile } = fileGraph;
|
|
301
|
+
return { loaded, errors, files: fileGraph.files, edges, programs: projects.map(item => item.program), program: projects[0]?.program ?? null, projects, ts, workspaces,
|
|
302
|
+
// reason: every file the checkers walk belongs to a program; an unknown file still answers null, as before.
|
|
303
|
+
checkerFor: (file) => (checkerByFile.get(helpers.canonical(file)) ?? null),
|
|
304
|
+
workspaceOf: (file) => helpers.workspaceOf(workspaces, helpers.canonical(file)) };
|
|
300
305
|
}
|