@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3
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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/src/components.ts
CHANGED
|
@@ -37,12 +37,31 @@ export interface Component {
|
|
|
37
37
|
* cycle with a live container is one, so a component has at least one.
|
|
38
38
|
*/
|
|
39
39
|
readonly roots: readonly string[];
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The source lines one deletion of the component removes: the distinct lines its
|
|
42
|
+
* members span, and none for a component of held declarations.
|
|
43
|
+
*/
|
|
41
44
|
readonly deletableLines: number;
|
|
45
|
+
/**
|
|
46
|
+
* Whether its members are held declarations, which no mechanical edit deletes: a dead
|
|
47
|
+
* declaration whose deletion changes a live declaration's value, and every dead
|
|
48
|
+
* declaration such a one references or whose deletion would change its value.
|
|
49
|
+
*/
|
|
50
|
+
readonly held: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* What the held-declaration rule reads of a graph, by place: the places of the later
|
|
55
|
+
* declarations whose value each one's deletion changes, and whether reachability holds
|
|
56
|
+
* each one live in some configuration.
|
|
57
|
+
*/
|
|
58
|
+
export interface Positional {
|
|
59
|
+
readonly dependents: readonly (readonly number[])[];
|
|
60
|
+
readonly live: readonly boolean[];
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
/** What a report carries for one dead component under one cascade mode. */
|
|
45
|
-
|
|
64
|
+
interface Listing {
|
|
46
65
|
readonly roots: readonly string[];
|
|
47
66
|
/** Every member under the full mode, and nothing under the default one. */
|
|
48
67
|
readonly members: readonly string[];
|
|
@@ -63,41 +82,97 @@ export function listing(component: Component, cascade: Cascade): Listing {
|
|
|
63
82
|
/**
|
|
64
83
|
* Groups the declarations `dead` marks into components, ordered by their first member's
|
|
65
84
|
* site; `dead`, `testOfDeadCode` and `unreferencedTest` hold one flag per declaration of
|
|
66
|
-
* the graph. Root members sit on the cycles no other cycle references. A
|
|
67
|
-
*
|
|
68
|
-
*
|
|
85
|
+
* the graph. Root members sit on the cycles no other cycle references. A component is a
|
|
86
|
+
* weakly connected part of the dead subgraph, because a cycle two roots reach is dead only
|
|
87
|
+
* through both. The held declarations `positional` decides form components of their own,
|
|
88
|
+
* and a dead declaration that is not held shares one with each of its dead dependents.
|
|
69
89
|
*/
|
|
70
90
|
export function componentsOf(
|
|
71
91
|
graph: Graph,
|
|
72
92
|
dead: readonly boolean[],
|
|
73
93
|
testOfDeadCode: readonly boolean[],
|
|
74
94
|
unreferencedTest: readonly boolean[] = [],
|
|
95
|
+
positional: Positional = { dependents: [], live: [] },
|
|
75
96
|
): readonly Component[] {
|
|
76
|
-
const { at, adjacent } = deadSubgraph(
|
|
97
|
+
const { at, position, adjacent, references } = deadSubgraph(
|
|
98
|
+
graph,
|
|
99
|
+
dead,
|
|
100
|
+
testOfDeadCode,
|
|
101
|
+
unreferencedTest,
|
|
102
|
+
);
|
|
77
103
|
if (at.length === 0) {
|
|
78
104
|
return [];
|
|
79
105
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
const dependents = at.map((index) =>
|
|
107
|
+
(positional.dependents[index] ?? []).map((to) => ({
|
|
108
|
+
to: position[to] ?? UNVISITED,
|
|
109
|
+
live: positional.live[to] === true,
|
|
110
|
+
})),
|
|
111
|
+
);
|
|
112
|
+
const held = heldSet(dependents, references);
|
|
113
|
+
const kept = adjacent.map((targets, from) => targets.filter((to) => held[to] === held[from]));
|
|
114
|
+
const { componentOf, members } = connected(kept);
|
|
115
|
+
const referenced = referencedCycles(kept, componentOf, members.length);
|
|
116
|
+
const groups = joined(
|
|
117
|
+
kept.map((targets, from) => [
|
|
118
|
+
...targets,
|
|
119
|
+
...(held[from] === true
|
|
120
|
+
? []
|
|
121
|
+
: (dependents[from] ?? []).flatMap(({ to }) =>
|
|
122
|
+
to === UNVISITED || held[to] === true ? [] : [to],
|
|
123
|
+
)),
|
|
124
|
+
]),
|
|
125
|
+
);
|
|
126
|
+
const idOf = (place: number): string => graph.symbols[at[place] ?? 0]?.id ?? "";
|
|
84
127
|
|
|
85
128
|
return groups.map((group, place) => {
|
|
86
|
-
const roots = group.filter((
|
|
129
|
+
const roots = group.filter((one) => {
|
|
87
130
|
// A dead member's dead container is on the member's own cycle, and the container
|
|
88
131
|
// is where the deletion starts.
|
|
89
|
-
const container = graph.parent[at[
|
|
90
|
-
return referenced[componentOf[
|
|
132
|
+
const container = graph.parent[at[one] ?? 0] ?? OUTSIDE;
|
|
133
|
+
return referenced[componentOf[one] ?? 0] !== true && dead[container] !== true;
|
|
91
134
|
});
|
|
135
|
+
const isHeld = group.every((one) => held[one] === true);
|
|
92
136
|
return {
|
|
93
137
|
id: componentId(place + 1),
|
|
94
138
|
members: group.map(idOf),
|
|
95
139
|
roots: roots.map(idOf),
|
|
96
|
-
deletableLines: linesSpanned(group.map((
|
|
140
|
+
deletableLines: isHeld ? 0 : linesSpanned(group.map((one) => graph.symbols[at[one] ?? 0])),
|
|
141
|
+
held: isHeld,
|
|
97
142
|
};
|
|
98
143
|
});
|
|
99
144
|
}
|
|
100
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Per place of the dead subgraph, whether the declaration is held: one with a live
|
|
148
|
+
* dependent, and each dead declaration a held one references or has as a dependent,
|
|
149
|
+
* until no further one joins.
|
|
150
|
+
*/
|
|
151
|
+
function heldSet(
|
|
152
|
+
dependents: readonly (readonly { readonly to: number; readonly live: boolean }[])[],
|
|
153
|
+
references: readonly (readonly number[])[],
|
|
154
|
+
): readonly boolean[] {
|
|
155
|
+
const held = dependents.map((after) => after.some((one) => one.live));
|
|
156
|
+
const before: number[][] = dependents.map(() => []);
|
|
157
|
+
dependents.forEach((after, from) => {
|
|
158
|
+
for (const { to } of after) {
|
|
159
|
+
if (to !== UNVISITED) {
|
|
160
|
+
before[to]?.push(from);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
const pending = held.flatMap((one, place) => (one ? [place] : []));
|
|
165
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
166
|
+
for (const reached of [...(references[next] ?? []), ...(before[next] ?? [])]) {
|
|
167
|
+
if (held[reached] !== true) {
|
|
168
|
+
held[reached] = true;
|
|
169
|
+
pending.push(reached);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return held;
|
|
174
|
+
}
|
|
175
|
+
|
|
101
176
|
/**
|
|
102
177
|
* The distinct source lines a set of declarations spans, each declaration at least the
|
|
103
178
|
* line it starts on. A member written inside its container's span adds no line, because
|
|
@@ -133,19 +208,24 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
|
|
|
133
208
|
}
|
|
134
209
|
|
|
135
210
|
/**
|
|
136
|
-
* The dead declarations by
|
|
137
|
-
*
|
|
138
|
-
* each reference one makes to another, except an unreferenced test-file declaration's
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
211
|
+
* The dead declarations by subgraph place (`at`, and `position` per graph declaration),
|
|
212
|
+
* a test file's only where it tests dead code or nothing the tests reach. `adjacent` holds
|
|
213
|
+
* each reference one makes to another, except an unreferenced test-file declaration's to
|
|
214
|
+
* production code, which decides nothing of its target; an edge each way between a dead
|
|
215
|
+
* member and its dead container; and one back from each production target of an admitted
|
|
216
|
+
* test, which puts the test in its subject's component. `references` holds the first alone.
|
|
142
217
|
*/
|
|
143
218
|
function deadSubgraph(
|
|
144
219
|
graph: Graph,
|
|
145
220
|
dead: readonly boolean[],
|
|
146
221
|
testOfDeadCode: readonly boolean[],
|
|
147
222
|
unreferencedTest: readonly boolean[],
|
|
148
|
-
): {
|
|
223
|
+
): {
|
|
224
|
+
readonly at: readonly number[];
|
|
225
|
+
readonly position: readonly number[];
|
|
226
|
+
readonly adjacent: readonly (readonly number[])[];
|
|
227
|
+
readonly references: readonly (readonly number[])[];
|
|
228
|
+
} {
|
|
149
229
|
const at: number[] = [];
|
|
150
230
|
const position = graph.symbols.map((_symbol, index) => {
|
|
151
231
|
const judged = testOfDeadCode[index] === true || unreferencedTest[index] === true;
|
|
@@ -156,6 +236,7 @@ function deadSubgraph(
|
|
|
156
236
|
return at.length - 1;
|
|
157
237
|
});
|
|
158
238
|
const adjacent: number[][] = at.map(() => []);
|
|
239
|
+
const references: number[][] = at.map(() => []);
|
|
159
240
|
at.forEach((index, from) => {
|
|
160
241
|
for (const edge of graph.out[index] ?? []) {
|
|
161
242
|
const to = position[edge.to] ?? UNVISITED;
|
|
@@ -163,6 +244,7 @@ function deadSubgraph(
|
|
|
163
244
|
continue;
|
|
164
245
|
}
|
|
165
246
|
adjacent[from]?.push(to);
|
|
247
|
+
references[from]?.push(to);
|
|
166
248
|
if (
|
|
167
249
|
testOfDeadCode[index] === true &&
|
|
168
250
|
graph.test[edge.to] !== true &&
|
|
@@ -177,7 +259,7 @@ function deadSubgraph(
|
|
|
177
259
|
adjacent[container]?.push(from);
|
|
178
260
|
}
|
|
179
261
|
});
|
|
180
|
-
return { at, adjacent };
|
|
262
|
+
return { at, position, adjacent, references };
|
|
181
263
|
}
|
|
182
264
|
|
|
183
265
|
/** One frame of the component walk: a declaration, and the next of its edges to follow. */
|
package/src/config.ts
CHANGED
|
@@ -50,7 +50,7 @@ export type Cascade = "roots" | "full";
|
|
|
50
50
|
* tags in effect. It is the shape a language built under build constraints reads,
|
|
51
51
|
* and this analyzer leaves it to that analysis.
|
|
52
52
|
*/
|
|
53
|
-
|
|
53
|
+
interface PlatformConfiguration {
|
|
54
54
|
readonly shape: "platform";
|
|
55
55
|
readonly id: string;
|
|
56
56
|
readonly os: string;
|
|
@@ -97,7 +97,7 @@ export interface Analysis {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** How findings are rendered. */
|
|
100
|
-
|
|
100
|
+
interface Reporters {
|
|
101
101
|
readonly formats: readonly Format[];
|
|
102
102
|
readonly sort: Sort;
|
|
103
103
|
readonly cascade: Cascade;
|
|
@@ -154,7 +154,7 @@ export type Provider =
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
/** The analyzers the orchestrator runs. */
|
|
157
|
-
|
|
157
|
+
interface Providers {
|
|
158
158
|
readonly analyzers: readonly Provider[];
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -293,7 +293,7 @@ export interface Inputs {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/** Which refusal a {@link ConfigError} carries. */
|
|
296
|
-
|
|
296
|
+
type ErrorKind =
|
|
297
297
|
/**
|
|
298
298
|
* A document that is not one JSON instance of the closed key list: a syntax
|
|
299
299
|
* error, a value of the wrong type, a value outside the closed set a key
|
|
@@ -69,11 +69,19 @@ export const RESOLVED_EXTENSIONS = [
|
|
|
69
69
|
".cjs",
|
|
70
70
|
] as const;
|
|
71
71
|
|
|
72
|
+
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
73
|
+
const MODULES_DIR = "node_modules";
|
|
74
|
+
|
|
72
75
|
/** The file's name, the last segment of its path. */
|
|
73
76
|
function basename(path: string): string {
|
|
74
77
|
return path.slice(path.lastIndexOf("/") + 1);
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/** Whether one path is the project's own: below the target root and in no `node_modules`. */
|
|
81
|
+
function isProjectPath(root: string, path: string): boolean {
|
|
82
|
+
return relativePath(root, path)?.split("/").includes(MODULES_DIR) === false;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
78
86
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
87
|
}
|
|
@@ -92,6 +100,8 @@ export function isConfigurationDocumentName(name: string): boolean {
|
|
|
92
100
|
interface ConfigurationDocument {
|
|
93
101
|
/** The file, absolute. */
|
|
94
102
|
readonly path: string;
|
|
103
|
+
/** The directories of the manifests whose dependencies the strings are read against. */
|
|
104
|
+
readonly manifestDirs: readonly string[];
|
|
95
105
|
/** Every string value at any depth, in document order; a member's name is no value. */
|
|
96
106
|
readonly strings: readonly string[];
|
|
97
107
|
}
|
|
@@ -100,11 +110,169 @@ interface ConfigurationDocument {
|
|
|
100
110
|
export type ParseFile = (fileName: string, text: string) => SourceFile;
|
|
101
111
|
|
|
102
112
|
/** One file no program holds that a configuration module outside the program leads to. */
|
|
103
|
-
|
|
113
|
+
interface OutsideModule {
|
|
104
114
|
/** The file, parsed alone. */
|
|
105
115
|
readonly file: SourceFile;
|
|
106
|
-
/** The
|
|
107
|
-
readonly
|
|
116
|
+
/** The directories of the manifests of the configuration files the file was reached from. */
|
|
117
|
+
readonly manifestDirs: readonly string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The flags a tool's command line names its configuration file with. */
|
|
121
|
+
const CONFIG_FLAGS: ReadonlySet<string> = new Set(["--config", "-c"]);
|
|
122
|
+
|
|
123
|
+
/** The extensions of a module a configuration flag may name. */
|
|
124
|
+
const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
|
|
125
|
+
|
|
126
|
+
/** The characters that end a word outside quotes and separate nothing: blanks. */
|
|
127
|
+
const BLANK = /[ \t\r\f\v]/u;
|
|
128
|
+
|
|
129
|
+
/** The characters that end a word outside quotes and begin an operator: newline included. */
|
|
130
|
+
const OPERATOR = /[\n;&|()<>]/u;
|
|
131
|
+
|
|
132
|
+
/** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
|
|
133
|
+
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The words of one command line, as POSIX token recognition reads quotes, backslashes,
|
|
137
|
+
* line continuations, comments and operators. A word an expansion may change (an unquoted
|
|
138
|
+
* `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
|
|
139
|
+
* is computed, and so is the rest of a line whose quote is never closed. A computed word
|
|
140
|
+
* and each operator are undefined, so a flag's file is never read across them.
|
|
141
|
+
*/
|
|
142
|
+
function shellWords(command: string): readonly (string | undefined)[] {
|
|
143
|
+
const words: (string | undefined)[] = [];
|
|
144
|
+
let word: string | undefined;
|
|
145
|
+
let computed = false;
|
|
146
|
+
const end = (): void => {
|
|
147
|
+
if (word !== undefined) {
|
|
148
|
+
words.push(computed ? undefined : word);
|
|
149
|
+
}
|
|
150
|
+
word = undefined;
|
|
151
|
+
computed = false;
|
|
152
|
+
};
|
|
153
|
+
for (let at = 0; at < command.length; at += 1) {
|
|
154
|
+
const char = command.charAt(at);
|
|
155
|
+
if (char === "\\" && command.charAt(at + 1) === "\n") {
|
|
156
|
+
at += 1;
|
|
157
|
+
} else if (BLANK.test(char)) {
|
|
158
|
+
end();
|
|
159
|
+
} else if (OPERATOR.test(char)) {
|
|
160
|
+
end();
|
|
161
|
+
words.push(undefined);
|
|
162
|
+
} else if (char === "#" && word === undefined) {
|
|
163
|
+
const newline = command.indexOf("\n", at);
|
|
164
|
+
at = newline < 0 ? command.length : newline - 1;
|
|
165
|
+
} else if (char === "\\") {
|
|
166
|
+
at += 1;
|
|
167
|
+
word = (word ?? "") + command.charAt(at);
|
|
168
|
+
} else if (char === "'" || char === '"') {
|
|
169
|
+
const close = closingQuote(command, at);
|
|
170
|
+
if (close < 0) {
|
|
171
|
+
word ??= "";
|
|
172
|
+
computed = true;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const quoted =
|
|
176
|
+
char === "'"
|
|
177
|
+
? { text: command.slice(at + 1, close), computed: false }
|
|
178
|
+
: doubleQuoted(command.slice(at + 1, close));
|
|
179
|
+
word = (word ?? "") + quoted.text;
|
|
180
|
+
computed ||= quoted.computed;
|
|
181
|
+
at = close;
|
|
182
|
+
} else {
|
|
183
|
+
computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
|
|
184
|
+
word = (word ?? "") + char;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
end();
|
|
188
|
+
return words;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
|
|
192
|
+
function doubleQuoted(inner: string): { readonly text: string; readonly computed: boolean } {
|
|
193
|
+
let text = "";
|
|
194
|
+
let computed = false;
|
|
195
|
+
for (let at = 0; at < inner.length; at += 1) {
|
|
196
|
+
const char = inner.charAt(at);
|
|
197
|
+
const next = inner.charAt(at + 1);
|
|
198
|
+
if (char === "\\" && next === "\n") {
|
|
199
|
+
at += 1;
|
|
200
|
+
} else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
201
|
+
text += next;
|
|
202
|
+
at += 1;
|
|
203
|
+
} else {
|
|
204
|
+
computed ||= char === "$" || char === "`";
|
|
205
|
+
text += char;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { text, computed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The index of the quote that closes the one at `open`, or -1 where none does. */
|
|
212
|
+
function closingQuote(command: string, open: number): number {
|
|
213
|
+
const quote = command.charAt(open);
|
|
214
|
+
for (let at = open + 1; at < command.length; at += 1) {
|
|
215
|
+
const char = command.charAt(at);
|
|
216
|
+
if (char === quote) {
|
|
217
|
+
return at;
|
|
218
|
+
}
|
|
219
|
+
if (quote === '"' && char === "\\") {
|
|
220
|
+
at += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The files the scripts of the manifest in one directory name after a configuration flag,
|
|
228
|
+
* `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
|
|
229
|
+
* the target root and in no `node_modules`.
|
|
230
|
+
*/
|
|
231
|
+
function scriptConfigurations(host: Host, root: string, dir: string): readonly string[] {
|
|
232
|
+
let scripts: unknown;
|
|
233
|
+
try {
|
|
234
|
+
const manifest: unknown = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
|
|
235
|
+
scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
|
|
236
|
+
} catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(scripts)) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const found: string[] = [];
|
|
243
|
+
for (const command of Object.values(scripts)) {
|
|
244
|
+
if (typeof command !== "string") {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const words = shellWords(command);
|
|
248
|
+
words.forEach((word, at) => {
|
|
249
|
+
const named =
|
|
250
|
+
word !== undefined && CONFIG_FLAGS.has(word)
|
|
251
|
+
? words[at + 1]
|
|
252
|
+
: word === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: /^--config=(.+)$/su.exec(word)?.[1];
|
|
255
|
+
const path = named === undefined ? undefined : resolvePath(dir, named);
|
|
256
|
+
if (
|
|
257
|
+
path !== undefined &&
|
|
258
|
+
[".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
|
|
259
|
+
isProjectPath(root, path) &&
|
|
260
|
+
host.kindOf(path) === "file"
|
|
261
|
+
) {
|
|
262
|
+
found.push(path);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The decoded value of one JSON file, or undefined where it cannot be read or parsed. */
|
|
270
|
+
function readDocument(host: Host, path: string): unknown {
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(host.readFile(path)) as unknown;
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
108
276
|
}
|
|
109
277
|
|
|
110
278
|
/** The configuration files one project holds and sits beside. */
|
|
@@ -117,7 +285,12 @@ interface ConfigurationFiles {
|
|
|
117
285
|
* the program does not hold either, read where a parser is given.
|
|
118
286
|
*/
|
|
119
287
|
readonly outside: readonly OutsideModule[];
|
|
120
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Every JSON configuration file in a directory of the project's files or above one, every
|
|
290
|
+
* JSON file a script's configuration flag names, and every JSON file below the target
|
|
291
|
+
* root and in no `node_modules` a relative string of either names, read against each
|
|
292
|
+
* manifest whose chain reaches it.
|
|
293
|
+
*/
|
|
121
294
|
readonly documents: readonly ConfigurationDocument[];
|
|
122
295
|
}
|
|
123
296
|
|
|
@@ -187,7 +360,8 @@ export function configurationFiles(
|
|
|
187
360
|
}
|
|
188
361
|
}
|
|
189
362
|
|
|
190
|
-
const
|
|
363
|
+
const pending: { path: string; manifestDir: string }[] = [];
|
|
364
|
+
const named: { path: string; manifestDir: string }[] = [];
|
|
191
365
|
for (const dir of [...dirs].sort()) {
|
|
192
366
|
if (!holdsManifest(dir)) {
|
|
193
367
|
continue;
|
|
@@ -200,24 +374,59 @@ export function configurationFiles(
|
|
|
200
374
|
.map((entry) => entry.name)
|
|
201
375
|
.sort();
|
|
202
376
|
} catch {
|
|
377
|
+
names = [];
|
|
378
|
+
}
|
|
379
|
+
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
380
|
+
for (const path of scriptConfigurations(host, root, dir)) {
|
|
381
|
+
const held = files.byName.get(path);
|
|
382
|
+
if (path.endsWith(".json")) {
|
|
383
|
+
pending.push({ path, manifestDir: dir });
|
|
384
|
+
} else if (held !== undefined) {
|
|
385
|
+
modules.push({ file: held, form: "a script's configuration flag" });
|
|
386
|
+
} else {
|
|
387
|
+
named.push({ path, manifestDir: dir });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// A document reached from two manifests is read against each, so a chain is keyed by both.
|
|
393
|
+
const documents = new Map<string, { path: string; manifestDirs: string[]; strings: string[] }>();
|
|
394
|
+
const unreadable = new Set<string>();
|
|
395
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
396
|
+
let document = documents.get(next.path);
|
|
397
|
+
if (unreadable.has(next.path) || document?.manifestDirs.includes(next.manifestDir) === true) {
|
|
203
398
|
continue;
|
|
204
399
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
value = JSON.parse(host.readFile(path));
|
|
210
|
-
} catch {
|
|
400
|
+
if (document === undefined) {
|
|
401
|
+
const value = readDocument(host, next.path);
|
|
402
|
+
if (value === undefined) {
|
|
403
|
+
unreadable.add(next.path);
|
|
211
404
|
continue;
|
|
212
405
|
}
|
|
213
|
-
|
|
406
|
+
document = { path: next.path, manifestDirs: [], strings: documentStrings(value) };
|
|
407
|
+
documents.set(next.path, document);
|
|
408
|
+
}
|
|
409
|
+
document.manifestDirs.push(next.manifestDir);
|
|
410
|
+
for (const text of document.strings) {
|
|
411
|
+
const path = RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix))
|
|
412
|
+
? resolvePath(dirnamePath(next.path), text)
|
|
413
|
+
: undefined;
|
|
414
|
+
if (
|
|
415
|
+
path?.endsWith(".json") === true &&
|
|
416
|
+
isProjectPath(root, path) &&
|
|
417
|
+
host.kindOf(path) === "file"
|
|
418
|
+
) {
|
|
419
|
+
pending.push({ path, manifestDir: next.manifestDir });
|
|
420
|
+
}
|
|
214
421
|
}
|
|
215
422
|
}
|
|
216
423
|
return {
|
|
217
424
|
modules,
|
|
218
|
-
documents,
|
|
425
|
+
documents: [...documents.values()],
|
|
219
426
|
outside:
|
|
220
|
-
parse === undefined
|
|
427
|
+
parse === undefined
|
|
428
|
+
? []
|
|
429
|
+
: outsideModules(host, root, [...dirs], files, holdsManifest, parse, named),
|
|
221
430
|
};
|
|
222
431
|
}
|
|
223
432
|
|
|
@@ -271,17 +480,19 @@ export function resolveRelativeSpecifier(
|
|
|
271
480
|
|
|
272
481
|
/**
|
|
273
482
|
* The configuration modules of the directories that hold a manifest which the program
|
|
274
|
-
* does not hold, and the files they import by a relative specifier,
|
|
275
|
-
* file joins. A file that cannot be read is skipped.
|
|
483
|
+
* does not hold, and the project files below `root` they import by a relative specifier,
|
|
484
|
+
* until no further file joins. A file that cannot be read is skipped.
|
|
276
485
|
*/
|
|
277
486
|
function outsideModules(
|
|
278
487
|
host: Host,
|
|
488
|
+
root: string,
|
|
279
489
|
dirs: readonly string[],
|
|
280
490
|
files: SourceFiles,
|
|
281
491
|
holdsManifest: (dir: string) => boolean,
|
|
282
492
|
parse: ParseFile,
|
|
493
|
+
named: readonly { readonly path: string; readonly manifestDir: string }[],
|
|
283
494
|
): readonly OutsideModule[] {
|
|
284
|
-
const pending: { path: string; manifestDir: string }[] = [];
|
|
495
|
+
const pending: { path: string; manifestDir: string }[] = [...named];
|
|
285
496
|
for (const dir of [...dirs].sort()) {
|
|
286
497
|
if (!holdsManifest(dir)) {
|
|
287
498
|
continue;
|
|
@@ -298,29 +509,37 @@ function outsideModules(
|
|
|
298
509
|
}
|
|
299
510
|
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
300
511
|
}
|
|
301
|
-
const found:
|
|
302
|
-
const
|
|
512
|
+
const found = new Map<string, { file: SourceFile; manifestDirs: string[] }>();
|
|
513
|
+
const unreadable = new Set<string>();
|
|
303
514
|
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
304
|
-
|
|
515
|
+
let outside = found.get(next.path);
|
|
516
|
+
if (
|
|
517
|
+
unreadable.has(next.path) ||
|
|
518
|
+
files.byName.has(next.path) ||
|
|
519
|
+
outside?.manifestDirs.includes(next.manifestDir) === true
|
|
520
|
+
) {
|
|
305
521
|
continue;
|
|
306
522
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
523
|
+
if (outside === undefined) {
|
|
524
|
+
let text: string;
|
|
525
|
+
try {
|
|
526
|
+
text = host.readFile(next.path);
|
|
527
|
+
} catch {
|
|
528
|
+
unreadable.add(next.path);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
outside = { file: parse(next.path, text), manifestDirs: [] };
|
|
532
|
+
found.set(next.path, outside);
|
|
313
533
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
for (const specifier of relativeSpecifiers(file)) {
|
|
534
|
+
outside.manifestDirs.push(next.manifestDir);
|
|
535
|
+
for (const specifier of relativeSpecifiers(outside.file)) {
|
|
317
536
|
const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
|
|
318
|
-
if (reached !== undefined) {
|
|
537
|
+
if (reached !== undefined && isProjectPath(root, reached)) {
|
|
319
538
|
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
320
539
|
}
|
|
321
540
|
}
|
|
322
541
|
}
|
|
323
|
-
return found;
|
|
542
|
+
return [...found.values()];
|
|
324
543
|
}
|
|
325
544
|
|
|
326
545
|
/** Every dependency the manifest in one directory declares, or none where it holds none to read. */
|
|
@@ -4,7 +4,7 @@ export const RECORD = {
|
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "5.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|