@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.1
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/dist/src/analysis.js +39 -16
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +27 -3
- package/dist/src/convention-rows.js +42 -0
- package/dist/src/conventions.js +48 -0
- package/dist/src/dependencies.js +109 -38
- package/dist/src/findings/completion.js +6 -7
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +371 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +60 -21
- package/dist/src/roots.js +96 -6
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +1 -0
- package/dist/src/sweep.js +29 -17
- package/dist/src/symbol-keys.js +114 -0
- package/dist/src/test-joins.js +101 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +21 -15
- package/dist/src/workspace-programs.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +45 -17
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +36 -4
- package/src/convention-rows.ts +57 -0
- package/src/conventions.ts +59 -0
- package/src/dependencies.ts +134 -46
- package/src/finding.ts +2 -0
- package/src/findings/completion.ts +7 -7
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/html-entries.ts +33 -15
- package/src/indexed-access.ts +468 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/kinds.ts +7 -6
- package/src/references.ts +86 -27
- package/src/roots.ts +129 -6
- package/src/session.ts +7 -1
- package/src/source-files.ts +3 -0
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/test-joins.ts +120 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a unique symbol keys, used through element accesses. `value[key]`, where
|
|
3
|
+
* `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
|
|
4
|
+
* whose computed name is that same constant, and a store through the access writes it.
|
|
5
|
+
* No name node resolves to such a member: the key's name resolves to the constant.
|
|
6
|
+
*/
|
|
7
|
+
import { isComputedPropertyName, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { isAnswered } from "./query.js";
|
|
9
|
+
/** The identifier an element access's argument names its key by, where it is a name. */
|
|
10
|
+
export function keyNameOf(argument) {
|
|
11
|
+
let at = argument;
|
|
12
|
+
while (isParenthesizedExpression(at)) {
|
|
13
|
+
at = at.expression;
|
|
14
|
+
}
|
|
15
|
+
if (isIdentifier(at)) {
|
|
16
|
+
return at;
|
|
17
|
+
}
|
|
18
|
+
return isPropertyAccessExpression(at) && isIdentifier(at.name) ? at.name : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The last identifier of each computed member name the inventory holds, `mark` for
|
|
22
|
+
* `[mark]` and `iterator` for `[Symbol.iterator]`: an access whose key is spelled by no
|
|
23
|
+
* such identifier selects no member a unique symbol keys, and is not asked about.
|
|
24
|
+
*/
|
|
25
|
+
export function computedKeyNames(symbols) {
|
|
26
|
+
const names = new Set();
|
|
27
|
+
for (const symbol of symbols) {
|
|
28
|
+
const key = /\[([^\]]+)\]$/u.exec(symbol.name)?.[1];
|
|
29
|
+
const last = key?.split(".").pop()?.trim();
|
|
30
|
+
if (last !== undefined && /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(last)) {
|
|
31
|
+
names.add(last);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
/** Whether a computed name's expression is literal text, which names a member by its text. */
|
|
37
|
+
function isLiteralKey(expression) {
|
|
38
|
+
return (isStringLiteral(expression) ||
|
|
39
|
+
isNumericLiteral(expression) ||
|
|
40
|
+
isNoSubstitutionTemplateLiteral(expression));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The members each keyed access selects. The receivers' types are asked in one batch,
|
|
44
|
+
* each object type's properties once, and the key of every property declared under a
|
|
45
|
+
* computed name in one batch more; a property is selected where its key and the
|
|
46
|
+
* access's key are one symbol.
|
|
47
|
+
*/
|
|
48
|
+
export function keyedUses(project, chains, resolved, properties, cap) {
|
|
49
|
+
if (resolved.length === 0) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const queries = project.queries;
|
|
53
|
+
const ownPaths = project.ownPaths();
|
|
54
|
+
const types = queries.typesAt(resolved.map((one) => one.access.access.expression), cap);
|
|
55
|
+
const parts = new Map();
|
|
56
|
+
const partsOf = (type) => {
|
|
57
|
+
const known = parts.get(type.id);
|
|
58
|
+
if (known !== undefined) {
|
|
59
|
+
return known;
|
|
60
|
+
}
|
|
61
|
+
let found = [];
|
|
62
|
+
if (type.isUnionType() || type.isIntersectionType()) {
|
|
63
|
+
const constituents = queries.constituents(type);
|
|
64
|
+
found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
|
|
65
|
+
}
|
|
66
|
+
else if (type.isObjectType()) {
|
|
67
|
+
found = [type];
|
|
68
|
+
}
|
|
69
|
+
parts.set(type.id, found);
|
|
70
|
+
return found;
|
|
71
|
+
};
|
|
72
|
+
/** Each receiver's properties declared in the target under a computed name, with that name's expression. */
|
|
73
|
+
const candidates = new Map();
|
|
74
|
+
const keyNodes = [];
|
|
75
|
+
const candidatesOf = (type) => {
|
|
76
|
+
const known = candidates.get(type.id);
|
|
77
|
+
if (known !== undefined) {
|
|
78
|
+
return known;
|
|
79
|
+
}
|
|
80
|
+
const found = [];
|
|
81
|
+
const read = properties.of(type);
|
|
82
|
+
for (const property of isAnswered(read.properties) ? read.properties : []) {
|
|
83
|
+
const handle = property.declarations.find((one) => ownPaths.has(one.path));
|
|
84
|
+
const node = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
85
|
+
const name = node?.name;
|
|
86
|
+
if (name !== undefined && isComputedPropertyName(name) && !isLiteralKey(name.expression)) {
|
|
87
|
+
found.push({ property, key: name.expression });
|
|
88
|
+
keyNodes.push(name.expression);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
candidates.set(type.id, found);
|
|
92
|
+
return found;
|
|
93
|
+
};
|
|
94
|
+
const perAccess = resolved.map((_one, index) => {
|
|
95
|
+
const type = types[index];
|
|
96
|
+
return type === undefined || !isAnswered(type) ? [] : partsOf(type).flatMap(candidatesOf);
|
|
97
|
+
});
|
|
98
|
+
const keyOf = new Map();
|
|
99
|
+
const asked = [...new Set(keyNodes)];
|
|
100
|
+
(asked.length === 0
|
|
101
|
+
? []
|
|
102
|
+
: project.symbolsAt(asked.map((node) => project.handle(node)), cap)).forEach((symbol, index) => {
|
|
103
|
+
const node = asked[index];
|
|
104
|
+
if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
|
|
105
|
+
keyOf.set(node, symbol.id);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return resolved.flatMap((one, index) => {
|
|
109
|
+
const ids = (perAccess[index] ?? [])
|
|
110
|
+
.filter((candidate) => keyOf.get(candidate.key) === one.symbol.id)
|
|
111
|
+
.flatMap((candidate) => chains.declarationsOf(candidate.property));
|
|
112
|
+
return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The test files a derived matrix holds although no configuration's file list names them.
|
|
3
|
+
* The test runner compiles such a file with the options of the nearest compiler
|
|
4
|
+
* configuration at or above it, so the file joins that configuration's program as a file
|
|
5
|
+
* its list names, unless a `package.json` below that configuration's directory makes the
|
|
6
|
+
* file another package's, which its own test runner compiles.
|
|
7
|
+
*/
|
|
8
|
+
import { sourcePaths } from "./file-facts.js";
|
|
9
|
+
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
10
|
+
import { writtenPath } from "./workspace-programs.js";
|
|
11
|
+
const PREFERRED = "/tsconfig.json";
|
|
12
|
+
/** The configurations of one directory in the order a file joins them: `tsconfig.json` first. */
|
|
13
|
+
function byPreference(a, b) {
|
|
14
|
+
const first = a.endsWith(PREFERRED) ? 0 : 1;
|
|
15
|
+
const second = b.endsWith(PREFERRED) ? 0 : 1;
|
|
16
|
+
return first - second || (a < b ? -1 : a > b ? 1 : 0);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Per configuration of `configFiles`, the absolute paths of the test files that join it:
|
|
20
|
+
* each file below `root` a pattern of `patterns` names, that no configuration's file list
|
|
21
|
+
* holds, joins the configuration nearest at or above its directory when no `package.json`
|
|
22
|
+
* sits between them. A configuration the compiler cannot read lists nothing and is joined
|
|
23
|
+
* by nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function joinedTestFiles(engine, host, root, configFiles, patterns) {
|
|
26
|
+
const listed = new Set();
|
|
27
|
+
const byDir = new Map();
|
|
28
|
+
for (const configFile of configFiles) {
|
|
29
|
+
let fileNames;
|
|
30
|
+
try {
|
|
31
|
+
fileNames = engine.parseConfigFile(configFile).fileNames;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
fileNames.forEach((name) => listed.add(name));
|
|
37
|
+
const dir = dirnamePath(configFile);
|
|
38
|
+
byDir.set(dir, [...(byDir.get(dir) ?? []), configFile].sort(byPreference));
|
|
39
|
+
}
|
|
40
|
+
const joins = new Map();
|
|
41
|
+
for (const path of sourcePaths(host, root)) {
|
|
42
|
+
const file = joinPath(root, path);
|
|
43
|
+
if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (let dir = dirnamePath(file);; dir = dirnamePath(dir)) {
|
|
47
|
+
const [nearest] = byDir.get(dir) ?? [];
|
|
48
|
+
if (nearest !== undefined) {
|
|
49
|
+
joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (host.kindOf(joinPath(dir, "package.json")) === "file" ||
|
|
53
|
+
dir === root ||
|
|
54
|
+
relativePath(root, dir) === undefined) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return joins;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How the snapshot reads each configuration a test file joins: the program `programs`
|
|
63
|
+
* describes with the files added to its root files, or a configuration only the snapshot
|
|
64
|
+
* holds that extends the one `programs` writes, or the configuration itself, and lists
|
|
65
|
+
* every file the configuration lists and the joined ones.
|
|
66
|
+
*/
|
|
67
|
+
export function withJoinedTests(engine, host, programs, joins) {
|
|
68
|
+
const found = new Map(programs);
|
|
69
|
+
for (const [configFile, files] of joins) {
|
|
70
|
+
const program = programs.get(configFile);
|
|
71
|
+
if (program?.kind === "program") {
|
|
72
|
+
found.set(configFile, {
|
|
73
|
+
kind: "program",
|
|
74
|
+
params: {
|
|
75
|
+
...program.params,
|
|
76
|
+
rootFiles: [...(program.params.rootFiles ?? []), ...files],
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let listed;
|
|
82
|
+
try {
|
|
83
|
+
listed =
|
|
84
|
+
program?.kind === "configuration"
|
|
85
|
+
? engine.parseConfigJson(JSON.parse(program.text), program.file).fileNames
|
|
86
|
+
: engine.parseConfigFile(configFile).fileNames;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const value = program?.kind === "configuration"
|
|
92
|
+
? { ...JSON.parse(program.text), files: [...listed, ...files] }
|
|
93
|
+
: { extends: configFile, files: [...listed, ...files] };
|
|
94
|
+
found.set(configFile, {
|
|
95
|
+
kind: "configuration",
|
|
96
|
+
file: program?.kind === "configuration" ? program.file : writtenPath(host, configFile),
|
|
97
|
+
text: `${JSON.stringify({ ...value, include: [] }, null, 2)}\n`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
package/dist/src/version.js
CHANGED
|
@@ -50,12 +50,8 @@ export function runCommands(text) {
|
|
|
50
50
|
}
|
|
51
51
|
return found;
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
* `.github/workflows` name: a token equal to a file's path below the target root, with or
|
|
56
|
-
* without a leading `./`, whatever directory the step runs in.
|
|
57
|
-
*/
|
|
58
|
-
export function workflowEntries(host, targetRoot, files) {
|
|
53
|
+
/** Every token the `run:` steps of the workflow files in the target root's `.github/workflows` hold. */
|
|
54
|
+
export function workflowTokens(host, targetRoot) {
|
|
59
55
|
const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
|
|
60
56
|
const found = [];
|
|
61
57
|
for (const path of workflowFiles(host, dir)) {
|
|
@@ -67,15 +63,25 @@ export function workflowEntries(host, targetRoot, files) {
|
|
|
67
63
|
continue;
|
|
68
64
|
}
|
|
69
65
|
for (const command of runCommands(text)) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
found.push(...scriptTokens(command));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The own files the `run:` steps of every workflow file in the target root's
|
|
73
|
+
* `.github/workflows` name: a token equal to a file's path below the target root, with or
|
|
74
|
+
* without a leading `./`, whatever directory the step runs in.
|
|
75
|
+
*/
|
|
76
|
+
export function workflowEntries(host, targetRoot, files) {
|
|
77
|
+
const found = [];
|
|
78
|
+
for (const token of workflowTokens(host, targetRoot)) {
|
|
79
|
+
if (COMPUTED.test(token)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
|
|
83
|
+
if (file !== undefined) {
|
|
84
|
+
found.push({ file, source: token });
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
return found;
|
|
@@ -48,7 +48,7 @@ function unstatable(reading) {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
/** The path beside `configFile` its written configuration takes, which no file has. */
|
|
51
|
-
function writtenPath(host, configFile) {
|
|
51
|
+
export function writtenPath(host, configFile) {
|
|
52
52
|
const stem = configFile.replace(/\.json$/u, "");
|
|
53
53
|
for (let counter = 0;; counter += 1) {
|
|
54
54
|
const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/analysis.ts
CHANGED
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
type Matrix,
|
|
85
85
|
type SweepResult,
|
|
86
86
|
} from "./matrix.ts";
|
|
87
|
-
import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
87
|
+
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
88
88
|
import type { UnansweredQuestion } from "./query.ts";
|
|
89
89
|
import { byPosition, isComponentFile, type Position } from "./position.ts";
|
|
90
90
|
import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
|
|
@@ -102,6 +102,7 @@ import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.t
|
|
|
102
102
|
import type { Scope } from "./scope.ts";
|
|
103
103
|
import { serializationContract } from "./serialization-contract.ts";
|
|
104
104
|
import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
|
|
105
|
+
import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
|
|
105
106
|
import { supportReferences, testSupportFiles } from "./test-support.ts";
|
|
106
107
|
import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
107
108
|
import { accessorsOf, storesOf } from "./stores.ts";
|
|
@@ -308,6 +309,9 @@ function readProjects<Answer>(
|
|
|
308
309
|
testFiles: config.ts.testFiles,
|
|
309
310
|
parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
|
|
310
311
|
publishedAPI: config.targetKind === "library",
|
|
312
|
+
publishedMembers: new Set(
|
|
313
|
+
(workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
|
|
314
|
+
),
|
|
311
315
|
};
|
|
312
316
|
const notBuilt: NotBuilt[] = [...discovered.notBuilt];
|
|
313
317
|
const dropped = (configFile: string, error: string): void => {
|
|
@@ -332,6 +336,10 @@ function readProjects<Answer>(
|
|
|
332
336
|
unbuildable.push(...met);
|
|
333
337
|
}
|
|
334
338
|
};
|
|
339
|
+
const testPatterns = config.ts.testFiles.map(globExpression);
|
|
340
|
+
const joins = discovered.derived
|
|
341
|
+
? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
|
|
342
|
+
: new Map<string, readonly string[]>();
|
|
335
343
|
const session = runSession(
|
|
336
344
|
engine,
|
|
337
345
|
[...discovered.configFiles, ...consumerOf.keys()],
|
|
@@ -460,14 +468,15 @@ function readProjects<Answer>(
|
|
|
460
468
|
return stage(project, { configuration, read });
|
|
461
469
|
},
|
|
462
470
|
discovered.derived ? "record" : "refuse",
|
|
463
|
-
|
|
471
|
+
joins.size === 0
|
|
472
|
+
? workspace?.programs
|
|
473
|
+
: withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins),
|
|
464
474
|
() => {
|
|
465
|
-
const patterns = config.ts.testFiles.map(globExpression);
|
|
466
475
|
outsideTests = testFilesAt(
|
|
467
476
|
host,
|
|
468
477
|
targetRoot,
|
|
469
478
|
sourcePaths(host, targetRoot).filter(
|
|
470
|
-
(path) => !heldFiles.has(path) &&
|
|
479
|
+
(path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path)),
|
|
471
480
|
),
|
|
472
481
|
options.parse,
|
|
473
482
|
);
|
|
@@ -1082,27 +1091,44 @@ function readEmitterRun(
|
|
|
1082
1091
|
}
|
|
1083
1092
|
|
|
1084
1093
|
/**
|
|
1085
|
-
* The workspace members whose
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1094
|
+
* The workspace members whose dependencies the run judges: `read` holds those a built
|
|
1095
|
+
* project holds a file below, `unbuilt` the others whose own directory, the nearest
|
|
1096
|
+
* manifest's above the configuration, holds a configuration the run could not build. A
|
|
1097
|
+
* member no configuration holds a file of is in neither.
|
|
1089
1098
|
*/
|
|
1090
|
-
function
|
|
1099
|
+
function judgedMembers(
|
|
1091
1100
|
host: Host,
|
|
1092
1101
|
targetRoot: string,
|
|
1093
1102
|
read: ReadRun<EmitterExtra>,
|
|
1094
|
-
): readonly string[] {
|
|
1103
|
+
): { readonly read: readonly string[]; readonly unbuilt: readonly string[] } {
|
|
1095
1104
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
1096
1105
|
const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1106
|
+
const owners = new Set(
|
|
1107
|
+
read.notBuilt.map((one) => nearestManifestDir(host, root, one.configFile)),
|
|
1108
|
+
);
|
|
1109
|
+
const judged = { read: [] as string[], unbuilt: [] as string[] };
|
|
1110
|
+
for (const dir of read.memberDirs) {
|
|
1099
1111
|
const below = relativePath(root, dir);
|
|
1100
1112
|
if (below === undefined) {
|
|
1101
|
-
|
|
1113
|
+
continue;
|
|
1102
1114
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1115
|
+
if ([...held].some((path) => path.startsWith(`${below}/`))) {
|
|
1116
|
+
judged.read.push(dir);
|
|
1117
|
+
} else if (owners.has(dir)) {
|
|
1118
|
+
judged.unbuilt.push(dir);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return judged;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** The directory of the nearest `package.json` at or above a file, inside `root`, else `root`. */
|
|
1125
|
+
function nearestManifestDir(host: Host, root: string, file: string): string {
|
|
1126
|
+
for (let dir = dirnamePath(file); relativePath(root, dir) !== undefined; dir = dirnamePath(dir)) {
|
|
1127
|
+
if (host.kindOf(joinPath(dir, "package.json")) === "file") {
|
|
1128
|
+
return dir;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return root;
|
|
1106
1132
|
}
|
|
1107
1133
|
|
|
1108
1134
|
/**
|
|
@@ -1120,6 +1146,7 @@ function emitterInputOver(
|
|
|
1120
1146
|
const targetRoot = scope.target.path;
|
|
1121
1147
|
const swept = sweptOf(read, input);
|
|
1122
1148
|
const { projects } = read;
|
|
1149
|
+
const members = judgedMembers(host, targetRoot, read);
|
|
1123
1150
|
return {
|
|
1124
1151
|
config,
|
|
1125
1152
|
swept,
|
|
@@ -1137,7 +1164,8 @@ function emitterInputOver(
|
|
|
1137
1164
|
targetRoot,
|
|
1138
1165
|
[...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
|
|
1139
1166
|
swept.sweep.candidates.map((candidate) => candidate.id),
|
|
1140
|
-
|
|
1167
|
+
members.read,
|
|
1168
|
+
members.unbuilt,
|
|
1141
1169
|
),
|
|
1142
1170
|
implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
|
|
1143
1171
|
files: {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "5.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
package/src/contextual-uses.ts
CHANGED
|
@@ -63,8 +63,13 @@ export interface ContextualUses {
|
|
|
63
63
|
readonly lookups: number;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The name one literal property is written under: its literal text, or the computed name
|
|
68
|
+
* node whose key the checker names, a unique symbol among them.
|
|
69
|
+
*/
|
|
70
|
+
function propertyName(
|
|
71
|
+
property: Node,
|
|
72
|
+
): { node: Node; text: string } | { node: Node; text: undefined } | undefined {
|
|
68
73
|
if (
|
|
69
74
|
!isPropertyAssignment(property) &&
|
|
70
75
|
!isShorthandPropertyAssignment(property) &&
|
|
@@ -83,6 +88,7 @@ function propertyName(property: Node): { node: Node; text: string } | undefined
|
|
|
83
88
|
if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
|
|
84
89
|
return { node: name, text: key.text };
|
|
85
90
|
}
|
|
91
|
+
return { node: name, text: undefined };
|
|
86
92
|
}
|
|
87
93
|
return undefined;
|
|
88
94
|
}
|
|
@@ -183,6 +189,31 @@ export function contextualUses<Brand>(
|
|
|
183
189
|
literals.map((one) => one.literal),
|
|
184
190
|
cap,
|
|
185
191
|
);
|
|
192
|
+
// A computed name that is not literal text names the property the literal declares
|
|
193
|
+
// under the checker's own key, which is how a type declares a member a unique symbol
|
|
194
|
+
// keys: asked once, for the literals that have a contextual type.
|
|
195
|
+
const computed = literals.flatMap(({ literal }, index) =>
|
|
196
|
+
contexts[index] === undefined || !isAnswered(contexts[index])
|
|
197
|
+
? []
|
|
198
|
+
: literal.properties.flatMap((property) => {
|
|
199
|
+
const name = propertyName(property);
|
|
200
|
+
return name !== undefined && name.text === undefined ? [name.node] : [];
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
batches += batchesOf(computed.length);
|
|
204
|
+
const keys = new Map<Node, string>();
|
|
205
|
+
(computed.length === 0
|
|
206
|
+
? []
|
|
207
|
+
: project.symbolsAt(
|
|
208
|
+
computed.map((node) => project.handle(node)),
|
|
209
|
+
cap,
|
|
210
|
+
)
|
|
211
|
+
).forEach((symbol, index) => {
|
|
212
|
+
const node = computed[index];
|
|
213
|
+
if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
|
|
214
|
+
keys.set(node, symbol.name);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
186
217
|
literals.forEach(({ literal, from }, index) => {
|
|
187
218
|
const context = contexts[index];
|
|
188
219
|
if (context === undefined || !isAnswered(context)) {
|
|
@@ -191,10 +222,11 @@ export function contextualUses<Brand>(
|
|
|
191
222
|
const tablesOf = partsOf(context).map(membersOf);
|
|
192
223
|
for (const property of literal.properties) {
|
|
193
224
|
const name = propertyName(property);
|
|
194
|
-
|
|
225
|
+
const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
|
|
226
|
+
if (name === undefined || text === undefined) {
|
|
195
227
|
continue;
|
|
196
228
|
}
|
|
197
|
-
const ids = [...new Set(tablesOf.flatMap((table) => table.get(
|
|
229
|
+
const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
|
|
198
230
|
if (ids.length > 0) {
|
|
199
231
|
writes.push({ at: name.node, from, ids });
|
|
200
232
|
}
|
package/src/convention-rows.ts
CHANGED
|
@@ -45,6 +45,14 @@ export interface HelperReading extends MoveReading {
|
|
|
45
45
|
readonly package: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** One member of a configuration file whose strings name the packages its tool loads. */
|
|
49
|
+
export interface PackageKey {
|
|
50
|
+
/** The member's name, matched at any depth of the objects the row reads. */
|
|
51
|
+
readonly key: string;
|
|
52
|
+
/** Globs naming the configuration files, matched in the manifest's directory alone. */
|
|
53
|
+
readonly files: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
/** One directory a row's globs name that a configuration property moves. */
|
|
49
57
|
export interface DirectoryMove {
|
|
50
58
|
/** The placeholder the row's globs and later moves write as `<id>`. */
|
|
@@ -83,6 +91,11 @@ export interface ConventionRow {
|
|
|
83
91
|
* string there names no file the tool runs or loads.
|
|
84
92
|
*/
|
|
85
93
|
readonly selections?: readonly MoveReading[];
|
|
94
|
+
/**
|
|
95
|
+
* The members whose strings name a package the tool loads: the package itself, a
|
|
96
|
+
* subpath of it, or the package followed by `:` and a name.
|
|
97
|
+
*/
|
|
98
|
+
readonly packageKeys?: readonly PackageKey[];
|
|
86
99
|
/** The members of a `package.json` the tool reads its configuration from. */
|
|
87
100
|
readonly manifestKeys?: readonly string[];
|
|
88
101
|
/** The settings that make the tool's output import a runtime helper package. */
|
|
@@ -131,8 +144,40 @@ const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
|
|
|
131
144
|
const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
|
|
132
145
|
const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
|
|
133
146
|
|
|
147
|
+
/** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
|
|
148
|
+
const ANGULAR_ENTRIES = [
|
|
149
|
+
"src/{main,main.server,server,polyfills,test}.ts",
|
|
150
|
+
"server.ts",
|
|
151
|
+
"projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
|
|
152
|
+
"projects/*/server.ts",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/** The `angular.json` members whose strings name the packages a build loads. */
|
|
156
|
+
const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
|
|
157
|
+
"builder",
|
|
158
|
+
"polyfills",
|
|
159
|
+
"styles",
|
|
160
|
+
"scripts",
|
|
161
|
+
].map((key) => ({ key, files: ["angular.json"] }));
|
|
162
|
+
|
|
134
163
|
/** Every row, ordered by name and then by range. */
|
|
135
164
|
export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
165
|
+
{
|
|
166
|
+
name: "angular",
|
|
167
|
+
package: "@angular/build",
|
|
168
|
+
range: ">=17.0.0 <22.0.0",
|
|
169
|
+
entries: ANGULAR_ENTRIES,
|
|
170
|
+
moves: [],
|
|
171
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "angular-devkit",
|
|
175
|
+
package: "@angular-devkit/build-angular",
|
|
176
|
+
range: ">=12.0.0 <22.0.0",
|
|
177
|
+
entries: ANGULAR_ENTRIES,
|
|
178
|
+
moves: [],
|
|
179
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
180
|
+
},
|
|
136
181
|
{
|
|
137
182
|
name: "astro",
|
|
138
183
|
package: "astro",
|
|
@@ -564,6 +609,18 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
564
609
|
},
|
|
565
610
|
],
|
|
566
611
|
},
|
|
612
|
+
{
|
|
613
|
+
name: "vitepress",
|
|
614
|
+
package: "vitepress",
|
|
615
|
+
range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
|
|
616
|
+
entries: [
|
|
617
|
+
`.vitepress/config.${ANY_MODULE}`,
|
|
618
|
+
`.vitepress/theme/index.${ANY_MODULE}`,
|
|
619
|
+
`**/*.data.${ANY_MODULE}`,
|
|
620
|
+
"**/*.vue",
|
|
621
|
+
],
|
|
622
|
+
moves: [],
|
|
623
|
+
},
|
|
567
624
|
{
|
|
568
625
|
name: "vitest",
|
|
569
626
|
package: "vitest",
|