@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.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/dist/src/analysis.js +108 -10
- package/dist/src/augmentations.js +62 -18
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +98 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +172 -40
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +30 -5
- package/dist/src/findings/visibility-narrowing.js +29 -5
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +57 -10
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +38 -4
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +8 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +119 -18
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +146 -11
- package/src/augmentations.ts +76 -22
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +128 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +15 -0
- package/src/findings/completion.ts +222 -46
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +36 -5
- package/src/findings/visibility-narrowing.ts +40 -5
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +104 -15
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +48 -3
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +10 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +155 -18
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a read through a value typed by a type parameter reaches: `value.kind`,
|
|
3
|
+
* where `value` is of a type parameter `T` of a generic function, reads `kind` on each
|
|
4
|
+
* object type no class declares that a call of the function passes for `T`, because the
|
|
5
|
+
* call's value is what the read reads, whatever member of the constraint the name resolves
|
|
6
|
+
* to. A class's members a constraint requires are interface satisfaction's.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isBinaryExpression,
|
|
11
|
+
isCallExpression,
|
|
12
|
+
isIdentifier,
|
|
13
|
+
isNewExpression,
|
|
14
|
+
isPropertyAccessExpression,
|
|
15
|
+
isTypeParameterDeclaration,
|
|
16
|
+
SyntaxKind,
|
|
17
|
+
type CallExpression,
|
|
18
|
+
type NewExpression,
|
|
19
|
+
type Node,
|
|
20
|
+
type PropertyAccessExpression,
|
|
21
|
+
type SourceFile,
|
|
22
|
+
} from "@typescript/native/ast";
|
|
23
|
+
import { SymbolFlags, type Type } from "@typescript/native/sync";
|
|
24
|
+
import type { AliasChains } from "./alias-chain.ts";
|
|
25
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
26
|
+
import { renderPosition } from "./position.ts";
|
|
27
|
+
import { isAnswered } from "./query.ts";
|
|
28
|
+
import type { Reference } from "./references.ts";
|
|
29
|
+
import type { ProjectView } from "./session.ts";
|
|
30
|
+
|
|
31
|
+
/** How deep a parameter's type is matched against the type a call passes: `readonly T[]` is one level. */
|
|
32
|
+
const MATCH_DEPTH = 3;
|
|
33
|
+
|
|
34
|
+
/** One read of a member through a value whose type is a type parameter. */
|
|
35
|
+
interface Read {
|
|
36
|
+
readonly file: SourceFile;
|
|
37
|
+
readonly from: string;
|
|
38
|
+
readonly access: PropertyAccessExpression;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A node that declares parameters, as a generic function does. */
|
|
42
|
+
type Generic = Node & {
|
|
43
|
+
readonly parameters?: readonly Node[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The name a call reaches one generic by: its own, or that of what holds it. */
|
|
47
|
+
function nameOf(generic: Generic): string | undefined {
|
|
48
|
+
const own = (generic as { readonly name?: Node }).name;
|
|
49
|
+
const holder = (generic.parent as { readonly name?: Node } | undefined)?.name;
|
|
50
|
+
const name = own ?? holder;
|
|
51
|
+
return name !== undefined && isIdentifier(name) ? name.text : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a property access is the target of a plain assignment, which writes the member. */
|
|
55
|
+
function assignedTo(access: PropertyAccessExpression): boolean {
|
|
56
|
+
const parent = access.parent;
|
|
57
|
+
return (
|
|
58
|
+
isBinaryExpression(parent) &&
|
|
59
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
60
|
+
parent.left === access
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The references the reads through type-parameter-typed values of one project's own files
|
|
66
|
+
* make to the members of the types its calls pass, each made by the declaration holding
|
|
67
|
+
* the read.
|
|
68
|
+
*/
|
|
69
|
+
export function typeParameterReads<Brand>(
|
|
70
|
+
project: ProjectView<Brand>,
|
|
71
|
+
held: Inventory,
|
|
72
|
+
chains: AliasChains,
|
|
73
|
+
root: string,
|
|
74
|
+
testFiles: ReadonlySet<string>,
|
|
75
|
+
): readonly Reference[] {
|
|
76
|
+
const queries = project.queries;
|
|
77
|
+
const reads: Read[] = [];
|
|
78
|
+
const calls: (CallExpression | NewExpression)[] = [];
|
|
79
|
+
for (const file of project.ownSourceFiles()) {
|
|
80
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
81
|
+
if (fileId === undefined) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const visit = (node: Node, from: string, generic: boolean): void => {
|
|
85
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
86
|
+
const inside =
|
|
87
|
+
generic ||
|
|
88
|
+
(node as { readonly typeParameters?: readonly Node[] }).typeParameters !== undefined;
|
|
89
|
+
if (
|
|
90
|
+
inside &&
|
|
91
|
+
isPropertyAccessExpression(node) &&
|
|
92
|
+
isIdentifier(node.name) &&
|
|
93
|
+
!assignedTo(node)
|
|
94
|
+
) {
|
|
95
|
+
reads.push({ file, from: holder, access: node });
|
|
96
|
+
}
|
|
97
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
98
|
+
calls.push(node);
|
|
99
|
+
}
|
|
100
|
+
node.forEachChild((child) => {
|
|
101
|
+
visit(child, holder, inside);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
file.forEachChild((child) => {
|
|
105
|
+
visit(child, fileId, false);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (reads.length === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The reads whose receiver the checker types as a type parameter, by the generic declaring it.
|
|
113
|
+
const receivers = queries.typesAt(reads.map((read) => read.access.expression));
|
|
114
|
+
const byGeneric = new Map<Generic, { read: Read; parameter: Type }[]>();
|
|
115
|
+
reads.forEach((read, at) => {
|
|
116
|
+
const type = receivers[at];
|
|
117
|
+
if (type === undefined || !isAnswered(type) || !type.isTypeParameter()) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const symbol = queries.symbolOfType(type);
|
|
121
|
+
const declaration =
|
|
122
|
+
symbol === undefined || !isAnswered(symbol)
|
|
123
|
+
? undefined
|
|
124
|
+
: symbol.declarations
|
|
125
|
+
.map((handle) => project.declarationAt(handle)?.node)
|
|
126
|
+
.find((node) => node !== undefined && isTypeParameterDeclaration(node));
|
|
127
|
+
const generic = declaration?.parent as Generic | undefined;
|
|
128
|
+
if (generic !== undefined) {
|
|
129
|
+
byGeneric.set(generic, [...(byGeneric.get(generic) ?? []), { read, parameter: type }]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (byGeneric.size === 0) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The types `instantiated` holds where `declared` holds `parameter`, matched part for part. */
|
|
137
|
+
const passedFor = (
|
|
138
|
+
parameter: Type,
|
|
139
|
+
declared: Type,
|
|
140
|
+
instantiated: Type,
|
|
141
|
+
depth: number,
|
|
142
|
+
): readonly Type[] => {
|
|
143
|
+
if (declared.id === parameter.id) {
|
|
144
|
+
return [instantiated];
|
|
145
|
+
}
|
|
146
|
+
if (depth >= MATCH_DEPTH) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const a = queries.typeArguments(declared);
|
|
150
|
+
const b = queries.typeArguments(instantiated);
|
|
151
|
+
if (!isAnswered(a) || !isAnswered(b) || a.length !== b.length) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
return a.flatMap((one, at) => {
|
|
155
|
+
const other = b[at];
|
|
156
|
+
return other === undefined ? [] : passedFor(parameter, one, other, depth + 1);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Only a call spelling a generic's name is resolved: one request each.
|
|
161
|
+
const spelled = new Set([...byGeneric.keys()].flatMap((generic) => nameOf(generic) ?? []));
|
|
162
|
+
const found = new Map<string, Reference>();
|
|
163
|
+
for (const call of calls) {
|
|
164
|
+
const callee = call.expression;
|
|
165
|
+
const written = isIdentifier(callee)
|
|
166
|
+
? callee.text
|
|
167
|
+
: isPropertyAccessExpression(callee) && isIdentifier(callee.name)
|
|
168
|
+
? callee.name.text
|
|
169
|
+
: undefined;
|
|
170
|
+
if (written === undefined || !spelled.has(written)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const resolved = queries.resolvedSignature(call);
|
|
174
|
+
if (resolved === undefined || !isAnswered(resolved) || resolved.declaration === undefined) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const generic = project.declarationAt(resolved.declaration)?.node as Generic | undefined;
|
|
178
|
+
const pending = generic === undefined ? undefined : byGeneric.get(generic);
|
|
179
|
+
const declared = pending === undefined ? undefined : queries.genericOf(resolved);
|
|
180
|
+
if (pending === undefined || declared === undefined || !isAnswered(declared)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const count = (generic?.parameters ?? []).length;
|
|
184
|
+
for (const { read, parameter } of pending) {
|
|
185
|
+
const passed: Type[] = [];
|
|
186
|
+
for (let at = 0; at < count; at += 1) {
|
|
187
|
+
const own = queries.parameterType(declared, at);
|
|
188
|
+
const given = queries.typeAtPosition(resolved, at);
|
|
189
|
+
if (own !== undefined && isAnswered(own) && isAnswered(given)) {
|
|
190
|
+
passed.push(...passedFor(parameter, own, given, 0));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const name = read.access.name.text;
|
|
194
|
+
const position = renderPosition(read.file, root, read.access.name.getStart());
|
|
195
|
+
for (const type of passed) {
|
|
196
|
+
const parts = type.isUnionType() ? queries.constituents(type) : [type];
|
|
197
|
+
for (const part of isAnswered(parts) ? parts : []) {
|
|
198
|
+
const owner = queries.symbolOfType(part);
|
|
199
|
+
if (
|
|
200
|
+
!isAnswered(owner) ||
|
|
201
|
+
owner === undefined ||
|
|
202
|
+
(owner.flags & SymbolFlags.Class) !== 0
|
|
203
|
+
) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const properties = queries.propertiesOf(part);
|
|
207
|
+
for (const property of isAnswered(properties) ? properties : []) {
|
|
208
|
+
if (property.name !== name) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
for (const to of chains.declarationsOf(property)) {
|
|
212
|
+
found.set(`${read.from}\u0000${to}\u0000${String(read.access.name.pos)}`, {
|
|
213
|
+
from: read.from,
|
|
214
|
+
to,
|
|
215
|
+
position,
|
|
216
|
+
use: "read",
|
|
217
|
+
resolution: "batch",
|
|
218
|
+
test: testFiles.has(position.path),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return [...found.values()];
|
|
227
|
+
}
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { moduleIdentity } from "../consumers.ts";
|
|
|
12
12
|
import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
|
|
13
13
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
14
14
|
import type { Host } from "../host.ts";
|
|
15
|
+
import type { ManifestEntry } from "../manifest.ts";
|
|
15
16
|
import type { NotBuilt } from "../discover.ts";
|
|
16
17
|
import { partialNotes } from "../partial.ts";
|
|
17
18
|
import {
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
type Report,
|
|
23
24
|
type ReportConfiguration,
|
|
24
25
|
type ReportNotBuilt,
|
|
26
|
+
type ReportNote,
|
|
25
27
|
} from "../report.ts";
|
|
26
28
|
import { json } from "../reporters/json.ts";
|
|
27
29
|
import { RENDERINGS } from "../reporters/reporters.ts";
|
|
@@ -92,6 +94,25 @@ function runRelative(invoked: string, path: string): string {
|
|
|
92
94
|
return below;
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* One note per manifest entry the run reads back to no source file: what the entry names
|
|
99
|
+
* reaches nothing, so the findings its file would have rooted are reported.
|
|
100
|
+
*/
|
|
101
|
+
function unmappedNotes(
|
|
102
|
+
entries: readonly ManifestEntry[],
|
|
103
|
+
targetRoot: string,
|
|
104
|
+
): readonly ReportNote[] {
|
|
105
|
+
return entries.map((entry) => {
|
|
106
|
+
const named = relativePath(targetRoot, entry.path) ?? entry.path;
|
|
107
|
+
return {
|
|
108
|
+
kind: "unmapped-entry",
|
|
109
|
+
path: relativePath(targetRoot, entry.dir) ?? ".",
|
|
110
|
+
key: "ts.entry_files",
|
|
111
|
+
message: `the manifest's ${entry.member} names ${named}, which reads back to no source file of the target: name the file it is built from in ts.entry_files`,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
/** Each project the run dropped as a report names it, by its configuration file below the target. */
|
|
96
117
|
function notBuiltOf(dropped: readonly NotBuilt[], targetRoot: string): readonly ReportNotBuilt[] {
|
|
97
118
|
return dropped.map((one) => {
|
|
@@ -288,6 +309,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
288
309
|
typeErrorSkips: run.typeErrorSkips,
|
|
289
310
|
unanswered: unansweredCounts(analysis),
|
|
290
311
|
conventionsApplied: run.conventionsApplied,
|
|
312
|
+
notes: unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
|
|
291
313
|
});
|
|
292
314
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
293
315
|
verdict = verdictOf(result, config);
|
package/src/verbs/explain.ts
CHANGED
|
@@ -315,12 +315,15 @@ function reported(found: CompletedFinding): string {
|
|
|
315
315
|
/**
|
|
316
316
|
* Every finding of the run that names the declaration by its reference and reports at
|
|
317
317
|
* another position: a part of it, or a suppression record naming it. Each is published under
|
|
318
|
-
* the reference asked about,
|
|
318
|
+
* the reference asked about, or a parameter's under one it starts, so an explanation leaving
|
|
319
|
+
* it out would disagree with the report.
|
|
319
320
|
*/
|
|
320
321
|
function namedElsewhere(findings: readonly CompletedFinding[], subject: InventorySymbol): string {
|
|
321
322
|
return findings
|
|
322
323
|
.filter(
|
|
323
|
-
(found) =>
|
|
324
|
+
(found) =>
|
|
325
|
+
(found.symbol.holder ?? found.symbol.ref) === subject.ref &&
|
|
326
|
+
positionKey(found.position) !== subject.id,
|
|
324
327
|
)
|
|
325
328
|
.map(
|
|
326
329
|
(found) =>
|
package/src/version.ts
CHANGED
package/src/workflow-steps.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The source files a CI workflow's `run:` steps
|
|
3
|
-
* A workflow file is read as text, line by line, with no
|
|
2
|
+
* The source files a CI workflow's `run:` steps and the repository's git hooks name by their
|
|
3
|
+
* path from the target root. A workflow or hook file is read as text, line by line, with no
|
|
4
|
+
* YAML parsed.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { SourceFile } from "@typescript/native/ast";
|
|
7
8
|
import type { Host } from "./host.ts";
|
|
8
9
|
import { scriptTokens } from "./manifest.ts";
|
|
9
|
-
import { joinPath, resolvePath } from "./paths.ts";
|
|
10
|
+
import { joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
10
11
|
import type { SourceFiles } from "./source-files.ts";
|
|
11
12
|
|
|
12
13
|
/** One file a workflow step names. */
|
|
@@ -21,6 +22,23 @@ const WORKFLOWS = ".github/workflows";
|
|
|
21
22
|
/** A line holding a run command: indentation and one sequence marker before `run:`. */
|
|
22
23
|
const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
|
|
23
24
|
|
|
25
|
+
/** The directories whose every file is a git hook script, run as the shell runs it. */
|
|
26
|
+
const HOOK_DIRS = [".husky", ".githooks"] as const;
|
|
27
|
+
|
|
28
|
+
/** The hook manager files whose commands are written under a key, with that key. */
|
|
29
|
+
const HOOK_MANAGERS: readonly (readonly [string, RegExp])[] = [
|
|
30
|
+
[".pre-commit-config.yaml", /^(\s*(?:- )?)entry:(.*)$/u],
|
|
31
|
+
["lefthook.yml", RUN_LINE],
|
|
32
|
+
["lefthook.yaml", RUN_LINE],
|
|
33
|
+
[".lefthook.yml", RUN_LINE],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** The names GNU make reads a makefile under, in the order it tries them. */
|
|
37
|
+
const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"] as const;
|
|
38
|
+
|
|
39
|
+
/** The operators that end one shell command of a recipe line. */
|
|
40
|
+
const COMMAND_SEPARATORS = /&&|\|\||;|\|/u;
|
|
41
|
+
|
|
24
42
|
const BLOCK_SCALAR = /^[|>][+-]?$/u;
|
|
25
43
|
|
|
26
44
|
const QUOTED = /^(["'])(.*)\1$/su;
|
|
@@ -30,7 +48,8 @@ const COMPUTED = /[${*?[]/u;
|
|
|
30
48
|
|
|
31
49
|
const INDENT = /^\s*/u;
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
/** The files directly in one directory whose name `accepts`, in name order. */
|
|
52
|
+
function filesIn(host: Host, dir: string, accepts: (name: string) => boolean): readonly string[] {
|
|
34
53
|
let entries;
|
|
35
54
|
try {
|
|
36
55
|
entries = host.readDirectory(dir);
|
|
@@ -38,17 +57,25 @@ function workflowFiles(host: Host, dir: string): readonly string[] {
|
|
|
38
57
|
return [];
|
|
39
58
|
}
|
|
40
59
|
return entries
|
|
41
|
-
.filter((entry) => !entry.directory &&
|
|
60
|
+
.filter((entry) => !entry.directory && accepts(entry.name))
|
|
42
61
|
.map((entry) => joinPath(dir, entry.name))
|
|
43
62
|
.sort();
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
function readText(host: Host, path: string): string | undefined {
|
|
66
|
+
try {
|
|
67
|
+
return host.readFile(path);
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every command line one workflow file's `run:` steps, or the steps under `key`, hold. */
|
|
74
|
+
export function runCommands(text: string, key: RegExp = RUN_LINE): readonly string[] {
|
|
48
75
|
const lines = text.split(/\r?\n/u);
|
|
49
76
|
const found: string[] = [];
|
|
50
77
|
for (let at = 0; at < lines.length; at += 1) {
|
|
51
|
-
const match =
|
|
78
|
+
const match = key.exec(lines[at] ?? "");
|
|
52
79
|
if (match === null) {
|
|
53
80
|
continue;
|
|
54
81
|
}
|
|
@@ -69,28 +96,95 @@ export function runCommands(text: string): readonly string[] {
|
|
|
69
96
|
return found;
|
|
70
97
|
}
|
|
71
98
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Every token the recipe lines of the target root's makefile hold, each with the directory
|
|
101
|
+
* below the root it is read against: a recipe line is one that starts with a tab, joined
|
|
102
|
+
* with the lines a trailing `\` continues it onto, and make runs each in a shell of its
|
|
103
|
+
* own, so a `cd` moves the directory of the later commands of its line alone.
|
|
104
|
+
*/
|
|
105
|
+
function recipeTokens(host: Host, root: string): readonly { token: string; dir: string }[] {
|
|
106
|
+
const path = MAKEFILES.map((name) => joinPath(root, name)).find(
|
|
107
|
+
(candidate) => host.kindOf(candidate) === "file",
|
|
108
|
+
);
|
|
109
|
+
const text = path === undefined ? undefined : readText(host, path);
|
|
110
|
+
if (text === undefined) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const found: { token: string; dir: string }[] = [];
|
|
114
|
+
for (const line of text.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)) {
|
|
115
|
+
if (!line.startsWith("\t")) {
|
|
81
116
|
continue;
|
|
82
117
|
}
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
let dir = root;
|
|
119
|
+
for (const command of withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(
|
|
120
|
+
COMMAND_SEPARATORS,
|
|
121
|
+
)) {
|
|
122
|
+
const tokens = scriptTokens(command);
|
|
123
|
+
const [first, target, ...rest] = tokens;
|
|
124
|
+
if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
|
|
125
|
+
dir = resolvePath(dir, target);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const token of tokens) {
|
|
129
|
+
found.push({ token, dir: relativePath(root, dir) ?? "" });
|
|
130
|
+
}
|
|
85
131
|
}
|
|
86
132
|
}
|
|
87
133
|
return found;
|
|
88
134
|
}
|
|
89
135
|
|
|
136
|
+
/** One shell line up to its comment: a `#` that starts a word outside quotes. */
|
|
137
|
+
function withoutComment(line: string): string {
|
|
138
|
+
let quote = "";
|
|
139
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
140
|
+
const char = line[at];
|
|
141
|
+
if (quote !== "") {
|
|
142
|
+
if (char === quote) {
|
|
143
|
+
quote = "";
|
|
144
|
+
} else if (char === "\\" && quote === '"') {
|
|
145
|
+
at += 1;
|
|
146
|
+
}
|
|
147
|
+
} else if (char === "'" || char === '"') {
|
|
148
|
+
quote = char;
|
|
149
|
+
} else if (char === "\\") {
|
|
150
|
+
at += 1;
|
|
151
|
+
} else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
|
|
152
|
+
return line.slice(0, at);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return line;
|
|
156
|
+
}
|
|
157
|
+
|
|
90
158
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
159
|
+
* Every token the target root's workflow steps, git hooks and makefile hold: the `run:`
|
|
160
|
+
* steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
|
|
161
|
+
* and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
|
|
162
|
+
* of a lefthook file and the makefile's recipe lines.
|
|
163
|
+
*/
|
|
164
|
+
export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
|
|
165
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
166
|
+
const commands: string[] = [];
|
|
167
|
+
for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
|
|
168
|
+
commands.push(...runCommands(readText(host, path) ?? ""));
|
|
169
|
+
}
|
|
170
|
+
for (const dir of HOOK_DIRS) {
|
|
171
|
+
for (const path of filesIn(host, joinPath(root, dir), () => true)) {
|
|
172
|
+
commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const [name, key] of HOOK_MANAGERS) {
|
|
176
|
+
commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
|
|
177
|
+
}
|
|
178
|
+
return [
|
|
179
|
+
...commands.flatMap((command) => scriptTokens(command)),
|
|
180
|
+
...recipeTokens(host, root).map(({ token }) => token),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The own files the target root's workflow steps and git hooks name, as
|
|
186
|
+
* {@link workflowTokens} reads them: a token equal to a file's path below the target root,
|
|
187
|
+
* with or without a leading `./`, whatever directory the step runs in.
|
|
94
188
|
*/
|
|
95
189
|
export function workflowEntries(
|
|
96
190
|
host: Host,
|
|
@@ -107,5 +201,16 @@ export function workflowEntries(
|
|
|
107
201
|
found.push({ file, source: token });
|
|
108
202
|
}
|
|
109
203
|
}
|
|
204
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
205
|
+
for (const { token, dir } of recipeTokens(host, root)) {
|
|
206
|
+
const below =
|
|
207
|
+
COMPUTED.test(token) || dir === ""
|
|
208
|
+
? undefined
|
|
209
|
+
: relativePath(root, resolvePath(joinPath(root, dir), token));
|
|
210
|
+
const file = below === undefined ? undefined : files.byPath.get(below);
|
|
211
|
+
if (file !== undefined) {
|
|
212
|
+
found.push({ file, source: token });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
110
215
|
return found;
|
|
111
216
|
}
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from "@typescript/native/sync";
|
|
17
17
|
import { isStringLiteralLikeNode } from "@typescript/native/ast";
|
|
18
18
|
import type { StringLiteralLikeNode } from "@typescript/native/ast";
|
|
19
|
+
import { baseUrlPaths } from "./base-url.ts";
|
|
19
20
|
import type { Host } from "./host.ts";
|
|
20
21
|
import { dirnamePath, resolvePath } from "./paths.ts";
|
|
21
22
|
import type { Engine } from "./session.ts";
|
|
@@ -62,7 +63,8 @@ const BATCH = 2048;
|
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* The options a configuration's program is built with: its own, less the ones that
|
|
65
|
-
* lay out an emitted program's inputs and outputs
|
|
66
|
+
* lay out an emitted program's inputs and outputs, and with `paths` read against the
|
|
67
|
+
* effective `baseUrl` as {@link baseUrlPaths} states. The analysis emits nothing, and a
|
|
66
68
|
* member's source is outside the importer's root directory by construction, which
|
|
67
69
|
* the compiler refuses under such a layout.
|
|
68
70
|
*/
|
|
@@ -75,7 +77,9 @@ function analysisOptions(parsed: Parsed): CreateSnapshotProgramParams["compilerO
|
|
|
75
77
|
tsBuildInfoFile: _tsBuildInfoFile,
|
|
76
78
|
...kept
|
|
77
79
|
} = parsed.options;
|
|
78
|
-
|
|
80
|
+
const paths = baseUrlPaths(parsed.options);
|
|
81
|
+
const options = paths === undefined ? kept : { ...kept, paths };
|
|
82
|
+
return composite === true ? { ...options, declaration: true } : options;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
|
|
@@ -152,7 +156,7 @@ function configurationOf(
|
|
|
152
156
|
const raw: unknown = (options as Record<string, unknown>)["pathsBasePath"];
|
|
153
157
|
const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
|
|
154
158
|
const paths: Record<string, string[]> = {};
|
|
155
|
-
for (const [key, targets] of Object.entries(options.paths ?? {})) {
|
|
159
|
+
for (const [key, targets] of Object.entries(baseUrlPaths(options) ?? options.paths ?? {})) {
|
|
156
160
|
paths[key] = targets.map((target) => resolvePath(base, target));
|
|
157
161
|
}
|
|
158
162
|
for (const entry of reading.entries) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which file an import of a workspace package's name is read from
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the package
|
|
7
|
-
* import nothing answers for is unresolved.
|
|
2
|
+
* Which file an import of a workspace package's name is read from: the package
|
|
3
|
+
* `node_modules` links the importer to or, with no link, the one its manifest depends on.
|
|
4
|
+
* A source file the compiler reaches stands; any other file reads back to its source or to
|
|
5
|
+
* a manifest target that is source, an ignored file to the source its path mirrors, a file
|
|
6
|
+
* the package neither compiles nor writes is read as installed, and the rest is unresolved.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { emittedFrom } from "./emit-map.ts";
|
|
10
|
+
import { gitIgnored } from "./git-ignore.ts";
|
|
11
11
|
import type { Host } from "./host.ts";
|
|
12
12
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
13
13
|
import { satisfies } from "./version-range.ts";
|
|
@@ -232,6 +232,32 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
232
232
|
return found;
|
|
233
233
|
};
|
|
234
234
|
|
|
235
|
+
const root = workspace.members[0]?.dir;
|
|
236
|
+
const ignored = root === undefined ? () => false : gitIgnored(host, root);
|
|
237
|
+
const manifests = new Map<
|
|
238
|
+
string,
|
|
239
|
+
{ dir: string; manifest: Record<string, unknown> } | undefined
|
|
240
|
+
>();
|
|
241
|
+
/** The nearest manifest at or above one directory, up to the workspace's root. */
|
|
242
|
+
const nearestManifest = (
|
|
243
|
+
dir: string,
|
|
244
|
+
): { dir: string; manifest: Record<string, unknown> } | undefined => {
|
|
245
|
+
if (manifests.has(dir)) {
|
|
246
|
+
return manifests.get(dir);
|
|
247
|
+
}
|
|
248
|
+
let found: { dir: string; manifest: Record<string, unknown> } | undefined;
|
|
249
|
+
try {
|
|
250
|
+
const value: unknown = JSON.parse(host.readFile(joinPath(dir, "package.json")));
|
|
251
|
+
found = isRecord(value) ? { dir, manifest: value } : undefined;
|
|
252
|
+
} catch {
|
|
253
|
+
const up = dirnamePath(dir);
|
|
254
|
+
found =
|
|
255
|
+
up === dir || root === undefined || !inside(root, up) ? undefined : nearestManifest(up);
|
|
256
|
+
}
|
|
257
|
+
manifests.set(dir, found);
|
|
258
|
+
return found;
|
|
259
|
+
};
|
|
260
|
+
|
|
235
261
|
const linkOf = (candidates: readonly WorkspaceMember[], fromDir: string): Linked => {
|
|
236
262
|
const name = candidates[0]?.name ?? "";
|
|
237
263
|
const linked = linkedFrom(name, fromDir);
|
|
@@ -243,14 +269,16 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
243
269
|
if (scope?.name === name) {
|
|
244
270
|
return { kind: "member", member: scope };
|
|
245
271
|
}
|
|
246
|
-
|
|
272
|
+
// A package below the root that no workspace lists depends on a member as a member does.
|
|
273
|
+
const owner = scope ?? nearestManifest(fromDir);
|
|
274
|
+
const manifest = owner?.manifest;
|
|
247
275
|
const declared = DEPENDENCY_MEMBERS.flatMap((field) => {
|
|
248
276
|
const held = manifest?.[field];
|
|
249
277
|
const specifier = isRecord(held) ? held[name] : undefined;
|
|
250
278
|
return typeof specifier === "string" ? [specifier] : [];
|
|
251
279
|
});
|
|
252
280
|
const matched = candidates.filter((member) =>
|
|
253
|
-
declared.some((specifier) => dependsOn(specifier, member,
|
|
281
|
+
declared.some((specifier) => dependsOn(specifier, member, owner?.dir ?? fromDir)),
|
|
254
282
|
);
|
|
255
283
|
const [one, two] = matched;
|
|
256
284
|
if (one === undefined) {
|
|
@@ -265,6 +293,25 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
265
293
|
const compiled = (member: WorkspaceMember, path: string): boolean =>
|
|
266
294
|
member.configurations.some((configuration) => configuration.fileNames.has(path));
|
|
267
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The member's source an ignored output path mirrors: the path below one of its
|
|
298
|
+
* directories, the deepest first, read below the member's own directory as an output
|
|
299
|
+
* directory's tree mirrors its root directory's, which a build leaves in an ignored
|
|
300
|
+
* directory however its configuration writes it.
|
|
301
|
+
*/
|
|
302
|
+
const mirrored = (member: WorkspaceMember, path: string): string | undefined => {
|
|
303
|
+
for (let dir = dirnamePath(path); inside(member.dir, dir); dir = dirnamePath(dir)) {
|
|
304
|
+
const found = emittedFrom(path, { outDir: dir, sourceDirs: [member.dir] }).find(
|
|
305
|
+
(candidate) =>
|
|
306
|
+
SOURCE.test(candidate) && !ignored(candidate) && host.kindOf(candidate) === "file",
|
|
307
|
+
);
|
|
308
|
+
if (found !== undefined) {
|
|
309
|
+
return found;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return undefined;
|
|
313
|
+
};
|
|
314
|
+
|
|
268
315
|
/**
|
|
269
316
|
* The member's source one of its paths is, or is compiled from: a TypeScript source
|
|
270
317
|
* file, the source an output path maps back to, or a file one of the member's
|
|
@@ -286,6 +333,9 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
286
333
|
if (found !== undefined) {
|
|
287
334
|
return found;
|
|
288
335
|
}
|
|
336
|
+
if (ignored(path)) {
|
|
337
|
+
return mirrored(member, path);
|
|
338
|
+
}
|
|
289
339
|
return compiled(member, path) && host.kindOf(path) === "file" ? path : undefined;
|
|
290
340
|
};
|
|
291
341
|
|
|
@@ -352,10 +402,12 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
352
402
|
}
|
|
353
403
|
const { member } = linked;
|
|
354
404
|
// A member no compiler configuration builds is read as the package manager
|
|
355
|
-
// installed it, wherever the compiler's resolution found it
|
|
405
|
+
// installed it, wherever the compiler's resolution found it, unless what it found is
|
|
406
|
+
// a file the tree ignores, which no install wrote.
|
|
356
407
|
if (
|
|
357
408
|
answered !== undefined &&
|
|
358
|
-
(member.configurations.length === 0
|
|
409
|
+
((member.configurations.length === 0 && !ignored(answered.resolvedFileName)) ||
|
|
410
|
+
!inside(member.dir, answered.resolvedFileName))
|
|
359
411
|
) {
|
|
360
412
|
return { kind: "default" };
|
|
361
413
|
}
|