@cplieger/deadset-ts 8.0.0-dev.5 → 8.0.0-dev.7
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/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +3 -2
- package/dist/src/analysis.js +28 -46
- package/dist/src/automatic-types.js +10 -29
- package/dist/src/build-scripts.js +17 -29
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +2 -3
- package/dist/src/config.js +3 -2
- package/dist/src/configuration-files.js +27 -168
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +1084 -0
- package/dist/src/contract/report.generated.js +796 -0
- package/dist/src/conventions.js +20 -60
- package/dist/src/dependencies.js +25 -43
- package/dist/src/discover.js +13 -39
- package/dist/src/edges.js +7 -15
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +2 -8
- package/dist/src/entry-points.js +3 -10
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +3 -63
- package/dist/src/exempt.js +13 -40
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +11 -20
- package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
- package/dist/src/findings/emitters.js +4 -4
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +8 -7
- package/dist/src/findings/non-code-artifacts.js +5 -6
- package/dist/src/findings/reads-and-writes.js +11 -9
- package/dist/src/findings/self-check.js +10 -12
- package/dist/src/findings/unused-declarations.js +28 -27
- package/dist/src/findings/visibility-narrowing.js +9 -11
- package/dist/src/findings-pass.js +7 -14
- package/dist/src/fs/manifest.js +43 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/git-ignore.js +34 -25
- package/dist/src/html-entries.js +6 -33
- package/dist/src/in-operands.js +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/intra-function-parts.js +18 -26
- package/dist/src/inventory.js +2 -21
- package/dist/src/json-document.js +4 -0
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +4 -360
- package/dist/src/manifest.js +5 -43
- package/dist/src/mdx-entries.js +3 -26
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +3 -2
- package/dist/src/product.js +13 -0
- package/dist/src/references.js +14 -27
- package/dist/src/report-schema.js +272 -0
- package/dist/src/report.js +39 -42
- package/dist/src/reporters/sarif.js +4 -2
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +12 -26
- package/dist/src/roots.js +7 -17
- package/dist/src/run.js +10 -8
- package/dist/src/schema.js +4 -3
- package/dist/src/scope.js +4 -5
- package/dist/src/serialization-contract.js +4 -8
- package/dist/src/session.js +2 -1
- package/dist/src/setup-failure.js +2 -6
- package/dist/src/suppress-file.js +12 -13
- package/dist/src/suppress-inline.js +3 -2
- package/dist/src/suppress.js +29 -48
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +5 -15
- package/dist/src/test-globals.js +6 -28
- package/dist/src/test-joins.js +6 -6
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
- package/dist/src/type-errors.js +14 -38
- package/dist/src/unseen-loads.js +2 -9
- package/dist/src/unwrap.js +23 -0
- package/dist/src/verbs/analyze.js +22 -15
- package/dist/src/verbs/describe.js +7 -7
- package/dist/src/verbs/explain.js +7 -4
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +4 -2
- package/dist/src/verbs/print-retained.js +4 -2
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +4 -3
- package/dist/src/workflow-steps.js +9 -46
- package/dist/src/workspace-manifests.js +2 -2
- package/dist/src/workspace-programs.js +5 -3
- package/dist/src/workspace-resolution.js +13 -17
- package/dist/src/workspace.js +28 -116
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +29 -54
- package/src/automatic-types.ts +11 -29
- package/src/build-scripts.ts +17 -38
- package/src/component-layer.ts +4 -2
- package/src/components.ts +2 -4
- package/src/config.ts +11 -15
- package/src/configuration-files.ts +27 -170
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +1148 -0
- package/src/contract/report.generated.ts +1157 -0
- package/src/conventions.ts +26 -74
- package/src/dependencies.ts +32 -50
- package/src/discover.ts +14 -39
- package/src/edges.ts +7 -21
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +3 -9
- package/src/entry-points.ts +2 -13
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +5 -74
- package/src/exempt.ts +17 -47
- package/src/file-facts.ts +6 -24
- package/src/finding.ts +9 -9
- package/src/findings/completion.ts +22 -23
- package/src/findings/dependencies-and-module-machinery.ts +4 -7
- package/src/findings/emitter.ts +6 -7
- package/src/findings/emitters.ts +6 -6
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +8 -8
- package/src/findings/non-code-artifacts.ts +5 -7
- package/src/findings/reads-and-writes.ts +15 -12
- package/src/findings/self-check.ts +11 -17
- package/src/findings/unused-declarations.ts +28 -27
- package/src/findings/visibility-narrowing.ts +9 -12
- package/src/findings-pass.ts +7 -16
- package/src/fs/manifest.ts +52 -0
- package/src/fs/tree.ts +110 -0
- package/src/git-ignore.ts +38 -25
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/index.ts +1 -1
- package/src/intra-function-parts.ts +18 -28
- package/src/inventory.ts +2 -20
- package/src/json-document.ts +5 -0
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +6 -406
- package/src/manifest.ts +5 -46
- package/src/mdx-entries.ts +3 -26
- package/src/partial.ts +2 -1
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +3 -2
- package/src/product.ts +18 -0
- package/src/references.ts +14 -29
- package/src/report-schema.ts +364 -0
- package/src/report.ts +66 -229
- package/src/reporters/annotations.ts +2 -2
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +17 -12
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +31 -38
- package/src/roots.ts +7 -20
- package/src/run.ts +10 -14
- package/src/schema.ts +4 -3
- package/src/scope.ts +4 -10
- package/src/serialization-contract.ts +4 -9
- package/src/session.ts +2 -1
- package/src/setup-failure.ts +5 -7
- package/src/suppress-file.ts +11 -15
- package/src/suppress-inline.ts +2 -2
- package/src/suppress.ts +39 -55
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +5 -18
- package/src/test-globals.ts +9 -27
- package/src/test-joins.ts +6 -8
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/shell.ts +161 -0
- package/src/{yaml-document.ts → text/yaml.ts} +46 -0
- package/src/type-errors.ts +25 -36
- package/src/unseen-loads.ts +1 -12
- package/src/unwrap.ts +34 -0
- package/src/verbs/analyze.ts +38 -25
- package/src/verbs/describe.ts +13 -7
- package/src/verbs/explain.ts +7 -4
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +4 -2
- package/src/verbs/print-retained.ts +4 -2
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +0 -6
- package/src/verbs/version.ts +4 -3
- package/src/workflow-steps.ts +9 -44
- package/src/workspace-manifests.ts +2 -2
- package/src/workspace-programs.ts +5 -3
- package/src/workspace-resolution.ts +21 -17
- package/src/workspace.ts +35 -121
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/dist/src/version.js +0 -7
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
- package/src/version.ts +0 -7
package/src/suppress.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* withholds every such finding anyway, and stale otherwise.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
type CODE,
|
|
12
|
+
KIND_ROWS,
|
|
13
|
+
SYMBOL_SUBJECTS,
|
|
14
|
+
type Subject,
|
|
15
|
+
} from "./contract/contract.generated.ts";
|
|
10
16
|
import { ConfigError } from "./config.ts";
|
|
11
17
|
import type { Finding } from "./finding.ts";
|
|
12
18
|
import { positionKey, type Position } from "./position.ts";
|
|
@@ -15,15 +21,6 @@ import { isRef } from "./ref.ts";
|
|
|
15
21
|
/** Which document a suppression is written in, spelled as a finding carries it. */
|
|
16
22
|
export type Mechanism = "inline" | "ignore" | "baseline";
|
|
17
23
|
|
|
18
|
-
/** The code a suppression that carries no reason is reported under. */
|
|
19
|
-
export const SUPPRESSION_WITHOUT_REASON = "DS1701";
|
|
20
|
-
|
|
21
|
-
/** The code an ignore entry or a baseline row that names no path is reported under. */
|
|
22
|
-
export const UNSCOPED_ENTRY = "DS1702";
|
|
23
|
-
|
|
24
|
-
/** The code a suppression that withholds nothing is reported under. */
|
|
25
|
-
export const STALE_SUPPRESSION = "DS1703";
|
|
26
|
-
|
|
27
24
|
/**
|
|
28
25
|
* One suppression record: one code bound to one site. A directive naming two codes is two
|
|
29
26
|
* records with one reason, and a directive above a line that declares two symbols is one
|
|
@@ -49,7 +46,7 @@ export interface SuppressionRecord {
|
|
|
49
46
|
* nothing, and its finding reports the suppression as written.
|
|
50
47
|
*/
|
|
51
48
|
export interface Refusal {
|
|
52
|
-
readonly reported: typeof
|
|
49
|
+
readonly reported: typeof CODE.suppressionWithoutReason | typeof CODE.unscopedIgnoreEntry;
|
|
53
50
|
readonly code: string;
|
|
54
51
|
/** The reference the entry names, empty for a directive. */
|
|
55
52
|
readonly symbol: string;
|
|
@@ -158,48 +155,48 @@ export interface Ledger {
|
|
|
158
155
|
readonly withheld: (finding: Finding) => boolean;
|
|
159
156
|
}
|
|
160
157
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
]
|
|
158
|
+
/** The subject of the kind each live code names. */
|
|
159
|
+
const SUBJECT_OF_CODE: ReadonlyMap<string, Subject> = new Map(
|
|
160
|
+
KIND_ROWS.map((row): [string, Subject] => [row.code, row.subject]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/** The subject a finding about one symbol kind is about. */
|
|
164
|
+
function subjectOf(kind: string): Subject | undefined {
|
|
165
|
+
return Object.hasOwn(SYMBOL_SUBJECTS, kind)
|
|
166
|
+
? SYMBOL_SUBJECTS[kind as keyof typeof SYMBOL_SUBJECTS]
|
|
167
|
+
: undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether a subject is a row of a document rather than a declaration of the program. */
|
|
171
|
+
function rowOf(subject: Subject | undefined): boolean {
|
|
172
|
+
return subject === "file" || subject === "dependency" || subject === "record";
|
|
173
|
+
}
|
|
170
174
|
|
|
171
175
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* finding about one.
|
|
176
|
+
* Whether a finding's subject is a row of a document rather than a declaration. Apart
|
|
177
|
+
* from a dependency row, nothing withholds a finding about one.
|
|
175
178
|
*/
|
|
176
|
-
const ROW_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
177
|
-
["file", ["DS1501", "DS1502"]],
|
|
178
|
-
...DEPENDENCY_SUBJECTS,
|
|
179
|
-
["root", ["DS1704"]],
|
|
180
|
-
["configured-declaration", ["DS1706"]],
|
|
181
|
-
["suppression", ["DS1701", "DS1702", "DS1703"]],
|
|
182
|
-
["edge", ["DS1705"]],
|
|
183
|
-
]);
|
|
184
|
-
|
|
185
|
-
/** Whether a finding's subject is a row of a document rather than a declaration. */
|
|
186
179
|
export function isRowSubject(kind: string): boolean {
|
|
187
|
-
return
|
|
180
|
+
return rowOf(subjectOf(kind));
|
|
188
181
|
}
|
|
189
182
|
|
|
190
183
|
/** Whether a code reports a row of a document. */
|
|
191
184
|
export function isRowCode(code: string): boolean {
|
|
192
|
-
return
|
|
185
|
+
return rowOf(SUBJECT_OF_CODE.get(code));
|
|
193
186
|
}
|
|
194
187
|
|
|
195
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Whether a finding's subject is a dependency row. An ignore entry or a baseline row
|
|
190
|
+
* naming the row's reference and file withholds its finding and marks nothing, and an
|
|
191
|
+
* inline directive never binds one.
|
|
192
|
+
*/
|
|
196
193
|
export function isDependencySubject(kind: string): boolean {
|
|
197
|
-
return
|
|
194
|
+
return subjectOf(kind) === "dependency";
|
|
198
195
|
}
|
|
199
196
|
|
|
200
197
|
/** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
|
|
201
198
|
export function isDependencyCode(code: string): boolean {
|
|
202
|
-
return
|
|
199
|
+
return SUBJECT_OF_CODE.get(code) === "dependency";
|
|
203
200
|
}
|
|
204
201
|
|
|
205
202
|
/** Whether a code reports a row of a document that no record of any mechanism can name. */
|
|
@@ -208,33 +205,20 @@ export function isUnsuppressibleCode(code: string): boolean {
|
|
|
208
205
|
}
|
|
209
206
|
|
|
210
207
|
/**
|
|
211
|
-
*
|
|
208
|
+
* Whether a finding's subject is a part of the declaration its reference names: a part
|
|
212
209
|
* has no reference of its own, so a finding about one names the declaration that holds
|
|
213
210
|
* it, and the part is decided inside that declaration whatever uses it.
|
|
214
211
|
*/
|
|
215
|
-
const PART_SUBJECTS: ReadonlySet<string> = new Set([
|
|
216
|
-
"parameter",
|
|
217
|
-
"receiver",
|
|
218
|
-
"result",
|
|
219
|
-
"statement",
|
|
220
|
-
"case",
|
|
221
|
-
"store",
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
/** Whether a finding's subject is a part of the declaration its reference names. */
|
|
225
212
|
export function isPartSubject(kind: string): boolean {
|
|
226
|
-
return
|
|
213
|
+
return subjectOf(kind) === "part";
|
|
227
214
|
}
|
|
228
215
|
|
|
229
|
-
/** The prefix of the codes of the part kinds, the intra-function range. */
|
|
230
|
-
const PART_KIND_PREFIX = "DS18";
|
|
231
|
-
|
|
232
216
|
/**
|
|
233
217
|
* Whether a code is a part kind, whose record withholds its finding and marks nothing:
|
|
234
218
|
* the record says the part is wanted, not that the declaration holding it is.
|
|
235
219
|
*/
|
|
236
220
|
export function isPartKind(code: string): boolean {
|
|
237
|
-
return
|
|
221
|
+
return SUBJECT_OF_CODE.get(code) === "part";
|
|
238
222
|
}
|
|
239
223
|
|
|
240
224
|
/** One finding's identity: its code and the position of the thing it names. */
|
|
@@ -254,14 +238,14 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
|
254
238
|
if (record.code !== finding.code) {
|
|
255
239
|
return false;
|
|
256
240
|
}
|
|
257
|
-
if (
|
|
241
|
+
if (isDependencySubject(finding.symbol.kind)) {
|
|
258
242
|
return (
|
|
259
243
|
record.mechanism !== "inline" &&
|
|
260
244
|
record.symbol === finding.symbol.ref &&
|
|
261
245
|
record.path === finding.position.path
|
|
262
246
|
);
|
|
263
247
|
}
|
|
264
|
-
if (
|
|
248
|
+
if (isRowSubject(finding.symbol.kind)) {
|
|
265
249
|
return false;
|
|
266
250
|
}
|
|
267
251
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
package/src/symbol-keys.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
isIdentifier,
|
|
13
13
|
isNoSubstitutionTemplateLiteral,
|
|
14
14
|
isNumericLiteral,
|
|
15
|
-
isParenthesizedExpression,
|
|
16
15
|
isPropertyAccessExpression,
|
|
17
16
|
isSpreadElement,
|
|
18
17
|
isStringLiteral,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
type Node,
|
|
22
21
|
type SourceFile,
|
|
23
22
|
} from "@typescript/native/ast";
|
|
23
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
24
24
|
import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
|
|
25
25
|
import type { AliasChains } from "./alias-chain.ts";
|
|
26
26
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
@@ -52,10 +52,7 @@ interface KeyedUse {
|
|
|
52
52
|
|
|
53
53
|
/** The identifier an element access's argument names its key by, where it is a name. */
|
|
54
54
|
export function keyNameOf(argument: Node): Node | undefined {
|
|
55
|
-
|
|
56
|
-
while (isParenthesizedExpression(at)) {
|
|
57
|
-
at = at.expression;
|
|
58
|
-
}
|
|
55
|
+
const at = parenthesesRemoved(argument);
|
|
59
56
|
if (isIdentifier(at)) {
|
|
60
57
|
return at;
|
|
61
58
|
}
|
package/src/template-field.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type { SymbolKind } from "./inventory.ts";
|
|
|
20
20
|
import { memberNames } from "./member-names.ts";
|
|
21
21
|
import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
|
|
22
22
|
import { isComponentFile, type Position } from "./position.ts";
|
|
23
|
+
import { bytewise } from "./text/compare.ts";
|
|
24
|
+
import { filesBelow } from "./fs/tree.ts";
|
|
23
25
|
|
|
24
26
|
/** One template, by its path below the target root. */
|
|
25
27
|
interface TemplateFile {
|
|
@@ -50,14 +52,6 @@ const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE,
|
|
|
50
52
|
/** One identifier, as the grammar of the analyzed language spells one. */
|
|
51
53
|
const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
|
|
52
54
|
|
|
53
|
-
/** Two strings ordered bytewise. */
|
|
54
|
-
function compare(a: string, b: string): number {
|
|
55
|
-
if (a === b) {
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
return a < b ? -1 : 1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
55
|
/**
|
|
62
56
|
* Every template under the configured directories, read once for the run, in path
|
|
63
57
|
* order and each file once. A directory is named relative to the target root, and
|
|
@@ -69,15 +63,8 @@ function compare(a: string, b: string): number {
|
|
|
69
63
|
export function readTemplates(host: Host, targetRoot: string, analysis: Analysis): Templates {
|
|
70
64
|
const files = new Map<string, string>();
|
|
71
65
|
const read = (dir: string): void => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
const path = joinPath(dir, entry.name);
|
|
77
|
-
if (entry.directory) {
|
|
78
|
-
read(path);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
66
|
+
for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
|
|
67
|
+
const path = joinPath(dir, below);
|
|
81
68
|
const absolute = joinPath(targetRoot, path);
|
|
82
69
|
if (!files.has(path) && host.kindOf(absolute) === "file") {
|
|
83
70
|
files.set(path, host.readFile(absolute));
|
|
@@ -104,7 +91,7 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
|
|
|
104
91
|
delimiters: analysis.templateDelimiters,
|
|
105
92
|
files: [...files]
|
|
106
93
|
.map(([path, text]) => ({ path, text }))
|
|
107
|
-
.sort((a, b) =>
|
|
94
|
+
.sort((a, b) => bytewise(a.path, b.path)),
|
|
108
95
|
};
|
|
109
96
|
}
|
|
110
97
|
|
package/src/test-globals.ts
CHANGED
|
@@ -25,10 +25,12 @@ import {
|
|
|
25
25
|
} from "@typescript/native/ast";
|
|
26
26
|
import type { ParseFile } from "./configuration-files.ts";
|
|
27
27
|
import type { Host } from "./host.ts";
|
|
28
|
-
import { dirnamePath, joinPath } from "./paths.ts";
|
|
28
|
+
import { ancestors, dirnamePath, joinPath } from "./paths.ts";
|
|
29
29
|
import { UNANSWERED } from "./query.ts";
|
|
30
30
|
import type { Engine, ProjectView } from "./session.ts";
|
|
31
31
|
import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
|
|
32
|
+
import { filesIn } from "./fs/tree.ts";
|
|
33
|
+
import { holdsManifest, INSTALLED_PACKAGES } from "./fs/manifest.ts";
|
|
32
34
|
|
|
33
35
|
/** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
|
|
34
36
|
export const GLOBALS_RUNNER = "vitest";
|
|
@@ -120,16 +122,7 @@ function setsGlobals(file: SourceFile): boolean {
|
|
|
120
122
|
|
|
121
123
|
/** Whether the runner's configuration in one manifest's directory injects the globals. */
|
|
122
124
|
function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
names = host
|
|
126
|
-
.readDirectory(dir)
|
|
127
|
-
.filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
|
|
128
|
-
.map((entry) => entry.name);
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return names.some((name) => {
|
|
125
|
+
return filesIn(host, dir, (name) => RUNNER_CONFIG.test(name)).some((name) => {
|
|
133
126
|
const path = joinPath(dir, name);
|
|
134
127
|
try {
|
|
135
128
|
return setsGlobals(parse(path, host.readFile(path)));
|
|
@@ -141,26 +134,15 @@ function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
|
|
|
141
134
|
|
|
142
135
|
/** Whether a file declaring the globals is installed where the configuration resolves packages from. */
|
|
143
136
|
function globalsInstalled(host: Host, configFile: string): boolean {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (dirnamePath(dir) === dir) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
137
|
+
return ancestors(dirnamePath(configFile)).some(
|
|
138
|
+
(dir) =>
|
|
139
|
+
host.kindOf(joinPath(dir, INSTALLED_PACKAGES, GLOBALS_RUNNER, "globals.d.ts")) === "file",
|
|
140
|
+
);
|
|
152
141
|
}
|
|
153
142
|
|
|
154
143
|
/** The directory of the manifest nearest one configuration file, at or above its own. */
|
|
155
144
|
function manifestDir(host: Host, configFile: string): string | undefined {
|
|
156
|
-
|
|
157
|
-
if (host.kindOf(joinPath(dir, "package.json")) === "file") {
|
|
158
|
-
return dir;
|
|
159
|
-
}
|
|
160
|
-
if (dirnamePath(dir) === dir) {
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
145
|
+
return ancestors(dirnamePath(configFile)).find((dir) => holdsManifest(host, dir));
|
|
164
146
|
}
|
|
165
147
|
|
|
166
148
|
function typesOf(value: unknown): readonly string[] | undefined {
|
package/src/test-joins.ts
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { sourcePaths } from "./file-facts.ts";
|
|
10
10
|
import type { Host } from "./host.ts";
|
|
11
|
-
import { dirnamePath, joinPath
|
|
11
|
+
import { ancestors, dirnamePath, joinPath } from "./paths.ts";
|
|
12
12
|
import type { Engine } from "./session.ts";
|
|
13
13
|
import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
|
|
14
|
+
import { bytewise } from "./text/compare.ts";
|
|
15
|
+
import { holdsManifest } from "./fs/manifest.ts";
|
|
14
16
|
|
|
15
17
|
const PREFERRED = "/tsconfig.json";
|
|
16
18
|
|
|
@@ -18,7 +20,7 @@ const PREFERRED = "/tsconfig.json";
|
|
|
18
20
|
function byPreference(a: string, b: string): number {
|
|
19
21
|
const first = a.endsWith(PREFERRED) ? 0 : 1;
|
|
20
22
|
const second = b.endsWith(PREFERRED) ? 0 : 1;
|
|
21
|
-
return first - second || (a
|
|
23
|
+
return first - second || bytewise(a, b);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -54,17 +56,13 @@ export function joinedTestFiles(
|
|
|
54
56
|
if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
|
|
55
57
|
continue;
|
|
56
58
|
}
|
|
57
|
-
for (
|
|
59
|
+
for (const dir of ancestors(dirnamePath(file), root)) {
|
|
58
60
|
const [nearest] = byDir.get(dir) ?? [];
|
|
59
61
|
if (nearest !== undefined) {
|
|
60
62
|
joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
|
|
61
63
|
break;
|
|
62
64
|
}
|
|
63
|
-
if (
|
|
64
|
-
host.kindOf(joinPath(dir, "package.json")) === "file" ||
|
|
65
|
-
dir === root ||
|
|
66
|
-
relativePath(root, dir) === undefined
|
|
67
|
-
) {
|
|
65
|
+
if (holdsManifest(host, dir)) {
|
|
68
66
|
break;
|
|
69
67
|
}
|
|
70
68
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two strings in the order of their UTF-8 bytes, the order every set of a run is read in.
|
|
3
|
+
* That is code point order, which UTF-16 code unit order (`<` on strings) breaks above
|
|
4
|
+
* U+FFFF: a surrogate unit is below U+E000 while the code point it encodes is above U+FFFF.
|
|
5
|
+
*/
|
|
6
|
+
export function bytewise(a: string, b: string): number {
|
|
7
|
+
if (a === b) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
const length = Math.min(a.length, b.length);
|
|
11
|
+
for (let at = 0; at < length; at += 1) {
|
|
12
|
+
const x = a.charCodeAt(at);
|
|
13
|
+
const y = b.charCodeAt(at);
|
|
14
|
+
if (x !== y) {
|
|
15
|
+
return ranked(x) < ranked(y) ? -1 : 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return a.length < b.length ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One UTF-16 unit moved so that a surrogate ranks above every unit of U+E000 to U+FFFF. */
|
|
22
|
+
function ranked(unit: number): number {
|
|
23
|
+
if (unit >= 0xe000) {
|
|
24
|
+
return unit - 0x800;
|
|
25
|
+
}
|
|
26
|
+
return unit >= 0xd800 ? unit + 0x2000 : unit;
|
|
27
|
+
}
|
package/src/text/glob.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob patterns over paths in two forms. The configured form (the configuration, convention
|
|
3
|
+
* rows, bundler entries, workspace member patterns) has `*` and `?` within a segment, `**`
|
|
4
|
+
* across segments and brace lists. A `.gitignore` line has git's form: bracket expressions
|
|
5
|
+
* and backslash escapes, no brace lists (https://git-scm.com/docs/gitignore#_pattern_format).
|
|
6
|
+
* A configured root is not a glob: it names a symbol reference, and is read where it is used.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type GlobForm = "configured" | "gitignore";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One character of a pattern as the expression syntax reads it literally. The set escaped
|
|
13
|
+
* is closed: under the unicode flag an escape of any other character is refused outright.
|
|
14
|
+
*/
|
|
15
|
+
function quoted(character: string): string {
|
|
16
|
+
return /[.*+?^${}()|[\]\\/]/u.test(character) ? `\\${character}` : character;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The ASCII ranges each POSIX class a bracket expression may name holds, as git's own
|
|
21
|
+
* character table reads them, each range written as its first and last character.
|
|
22
|
+
*/
|
|
23
|
+
const CLASSES: ReadonlyMap<string, string> = new Map([
|
|
24
|
+
["alnum", "09AZaz"],
|
|
25
|
+
["alpha", "AZaz"],
|
|
26
|
+
["blank", "\t\t "],
|
|
27
|
+
["cntrl", "\0\x1f\x7f\x7f"],
|
|
28
|
+
["digit", "09"],
|
|
29
|
+
["graph", "!~"],
|
|
30
|
+
["lower", "az"],
|
|
31
|
+
["print", " ~"],
|
|
32
|
+
["punct", "!/:@[`{~"],
|
|
33
|
+
["space", "\t\n\r\r "],
|
|
34
|
+
["upper", "AZ"],
|
|
35
|
+
["xdigit", "09AFaf"],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** What a pattern git cannot read matches: nothing, as git aborts the whole match. */
|
|
39
|
+
const NOTHING = "(?!)";
|
|
40
|
+
|
|
41
|
+
/** One code point as a member of an expression class. */
|
|
42
|
+
function member(point: number): string {
|
|
43
|
+
return `\\u{${point.toString(16)}}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The class a bracket expression opening at `open` denotes, and the index past its `]`,
|
|
48
|
+
* read as git's wildmatch reads it: a leading `!` or `^` negates it, a `]` first is a
|
|
49
|
+
* member, `\` escapes one character, `x-y` is a range (one whose `y` is below `x` holds
|
|
50
|
+
* `x` alone) and `[:name:]` a POSIX class. An unclosed bracket or an unknown class name
|
|
51
|
+
* matches nothing. Neither form matches the solidus.
|
|
52
|
+
*/
|
|
53
|
+
function bracket(pattern: string, open: number): { source: string; end: number } {
|
|
54
|
+
let at = open + 1;
|
|
55
|
+
const negated = pattern[at] === "!" || pattern[at] === "^";
|
|
56
|
+
if (negated) {
|
|
57
|
+
at += 1;
|
|
58
|
+
}
|
|
59
|
+
const next = (): number | undefined => {
|
|
60
|
+
const point = pattern.codePointAt(at);
|
|
61
|
+
at += point !== undefined && point > 0xffff ? 2 : 1;
|
|
62
|
+
return point;
|
|
63
|
+
};
|
|
64
|
+
let members = "";
|
|
65
|
+
let low: number | undefined;
|
|
66
|
+
for (let first = true; ; first = false) {
|
|
67
|
+
let point = next();
|
|
68
|
+
if (point === undefined) {
|
|
69
|
+
return { source: NOTHING, end: pattern.length };
|
|
70
|
+
}
|
|
71
|
+
if (point === 0x5d && !first) {
|
|
72
|
+
return { source: negated ? `[^/${members}]` : `(?!/)[${members}]`, end: at };
|
|
73
|
+
}
|
|
74
|
+
if (point === 0x2d && low !== undefined && at < pattern.length && pattern[at] !== "]") {
|
|
75
|
+
let high = next();
|
|
76
|
+
if (high === 0x5c) {
|
|
77
|
+
high = next();
|
|
78
|
+
}
|
|
79
|
+
if (high === undefined) {
|
|
80
|
+
return { source: NOTHING, end: pattern.length };
|
|
81
|
+
}
|
|
82
|
+
members += high >= low ? `${member(low)}-${member(high)}` : "";
|
|
83
|
+
low = undefined;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (point === 0x5b && pattern[at] === ":") {
|
|
87
|
+
const close = pattern.indexOf("]", at + 1);
|
|
88
|
+
if (close < 0) {
|
|
89
|
+
return { source: NOTHING, end: pattern.length };
|
|
90
|
+
}
|
|
91
|
+
if (close > at + 1 && pattern[close - 1] === ":") {
|
|
92
|
+
const ranges = CLASSES.get(pattern.slice(at + 1, close - 1));
|
|
93
|
+
if (ranges === undefined) {
|
|
94
|
+
return { source: NOTHING, end: pattern.length };
|
|
95
|
+
}
|
|
96
|
+
for (let one = 0; one < ranges.length; one += 2) {
|
|
97
|
+
members += `${member(ranges.charCodeAt(one))}-${member(ranges.charCodeAt(one + 1))}`;
|
|
98
|
+
}
|
|
99
|
+
low = undefined;
|
|
100
|
+
at = close + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (point === 0x5c) {
|
|
105
|
+
point = next();
|
|
106
|
+
if (point === undefined) {
|
|
107
|
+
return { source: NOTHING, end: pattern.length };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
members += member(point);
|
|
111
|
+
low = point;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The expression source of one pattern with no brace list. */
|
|
116
|
+
function source(pattern: string, form: GlobForm): string {
|
|
117
|
+
let text = "";
|
|
118
|
+
let at = 0;
|
|
119
|
+
while (at < pattern.length) {
|
|
120
|
+
if (pattern.startsWith("**/", at)) {
|
|
121
|
+
text += "(?:[^/]*/)*";
|
|
122
|
+
at += 3;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (pattern.startsWith("**", at)) {
|
|
126
|
+
text += ".*";
|
|
127
|
+
at += 2;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const character = pattern[at] ?? "";
|
|
131
|
+
if (form === "gitignore" && character === "[") {
|
|
132
|
+
const read = bracket(pattern, at);
|
|
133
|
+
text += read.source;
|
|
134
|
+
at = read.end;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (form === "gitignore" && character === "\\" && at + 1 < pattern.length) {
|
|
138
|
+
text += quoted(pattern[at + 1] ?? "");
|
|
139
|
+
at += 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
at += 1;
|
|
143
|
+
text += character === "*" ? "[^/]*" : character === "?" ? "[^/]" : quoted(character);
|
|
144
|
+
}
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A pattern with every brace list expanded into its alternatives; a lone `{` stands for itself. */
|
|
149
|
+
export function braceExpanded(pattern: string): readonly string[] {
|
|
150
|
+
const open = pattern.indexOf("{");
|
|
151
|
+
const close = open < 0 ? -1 : pattern.indexOf("}", open);
|
|
152
|
+
if (close < 0) {
|
|
153
|
+
return [pattern];
|
|
154
|
+
}
|
|
155
|
+
const before = pattern.slice(0, open);
|
|
156
|
+
const after = pattern.slice(close + 1);
|
|
157
|
+
return pattern
|
|
158
|
+
.slice(open + 1, close)
|
|
159
|
+
.split(",")
|
|
160
|
+
.flatMap((member) => braceExpanded(before + member + after));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The expression one whole-path pattern of the configured form denotes. */
|
|
164
|
+
export function globExpression(pattern: string): RegExp {
|
|
165
|
+
const alternatives = braceExpanded(pattern).map((one) => source(one, "configured"));
|
|
166
|
+
return new RegExp(`^(?:${alternatives.join("|")})$`, "u");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The expression one `.gitignore` pattern denotes over a path below its file's directory. */
|
|
170
|
+
export function gitignoreExpression(pattern: string): RegExp {
|
|
171
|
+
return new RegExp(`^${source(pattern, "gitignore")}$`, "u");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const SEGMENTS = new Map<string, RegExp>();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether one directory name matches one segment of a workspace's member pattern, its brace
|
|
178
|
+
* lists already expanded. Neither `*` nor `?` matches a leading dot, as npm's workspaces read
|
|
179
|
+
* them: https://docs.npmjs.com/cli/using-npm/workspaces
|
|
180
|
+
*/
|
|
181
|
+
export function segmentMatches(segment: string, name: string): boolean {
|
|
182
|
+
if (name.startsWith(".") && !segment.startsWith(".")) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
let expression = SEGMENTS.get(segment);
|
|
186
|
+
if (expression === undefined) {
|
|
187
|
+
expression = globExpression(segment);
|
|
188
|
+
SEGMENTS.set(segment, expression);
|
|
189
|
+
}
|
|
190
|
+
return expression.test(name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Whether one path's segments match a member pattern's, `**` standing for any run of segments
|
|
195
|
+
* that starts with no dot.
|
|
196
|
+
*/
|
|
197
|
+
export function pathMatches(pattern: readonly string[], path: readonly string[]): boolean {
|
|
198
|
+
const [head, ...rest] = pattern;
|
|
199
|
+
if (head === undefined) {
|
|
200
|
+
return path.length === 0;
|
|
201
|
+
}
|
|
202
|
+
if (head === "**") {
|
|
203
|
+
return (
|
|
204
|
+
pathMatches(rest, path) ||
|
|
205
|
+
(path.length > 0 && !(path[0] ?? "").startsWith(".") && pathMatches(pattern, path.slice(1)))
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const [name, ...below] = path;
|
|
209
|
+
return name !== undefined && segmentMatches(head, name) && pathMatches(rest, below);
|
|
210
|
+
}
|