@cplieger/deadset-ts 8.0.0-dev.6 → 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 +25 -43
- 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 +2 -1
- package/dist/src/configuration-files.js +27 -168
- package/dist/src/contract/contract.generated.js +13 -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 +2 -4
- package/dist/src/exempt.js +6 -22
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/reads-and-writes.js +2 -1
- package/dist/src/findings/self-check.js +3 -2
- package/dist/src/findings-pass.js +6 -11
- 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/intra-function-parts.js +10 -13
- 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/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 +15 -16
- package/dist/src/report.js +22 -31
- package/dist/src/reporters/sarif.js +4 -2
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +11 -20
- package/dist/src/roots.js +7 -17
- package/dist/src/run.js +8 -7
- 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/suppress-file.js +8 -10
- 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 +15 -13
- package/dist/src/verbs/describe.js +4 -4
- package/dist/src/verbs/explain.js +5 -3
- package/dist/src/verbs/print-projects.js +2 -1
- package/dist/src/verbs/print-retained.js +2 -1
- package/dist/src/verbs/print-roots.js +2 -1
- package/dist/src/verbs/version.js +2 -1
- 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 +1 -1
- package/src/analysis.ts +26 -51
- 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 +4 -2
- package/src/configuration-files.ts +27 -170
- package/src/contract/contract.generated.ts +25 -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 +4 -4
- package/src/exempt.ts +5 -27
- package/src/file-facts.ts +6 -24
- package/src/findings/reads-and-writes.ts +2 -1
- package/src/findings/self-check.ts +3 -2
- package/src/findings-pass.ts +6 -12
- 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/intra-function-parts.ts +10 -14
- package/src/inventory.ts +2 -20
- package/src/json-document.ts +5 -0
- package/src/keyof-comparisons.ts +3 -7
- 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 +15 -17
- package/src/report.ts +24 -34
- package/src/reporters/sarif.ts +4 -4
- package/src/reporters/template.ts +4 -20
- package/src/resolve.ts +20 -27
- package/src/roots.ts +7 -20
- package/src/run.ts +8 -13
- 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/suppress-file.ts +8 -11
- 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 +22 -21
- package/src/verbs/describe.ts +4 -4
- package/src/verbs/explain.ts +5 -3
- package/src/verbs/print-projects.ts +2 -1
- package/src/verbs/print-retained.ts +2 -1
- package/src/verbs/print-roots.ts +2 -1
- package/src/verbs/version.ts +2 -1
- 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/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type SourceFile,
|
|
54
54
|
type SpreadAssignment,
|
|
55
55
|
} from "@typescript/native/ast";
|
|
56
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
56
57
|
import {
|
|
57
58
|
SignatureKind,
|
|
58
59
|
SymbolFlags,
|
|
@@ -227,12 +228,6 @@ interface MethodForm {
|
|
|
227
228
|
readonly invocation: Invocation;
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
function unparenthesized(expression: Expression): Expression {
|
|
231
|
-
return isParenthesizedExpression(expression)
|
|
232
|
-
? unparenthesized(expression.expression)
|
|
233
|
-
: expression;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
231
|
/**
|
|
237
232
|
* The invocation a call writes through a method of `Function.prototype`, read from
|
|
238
233
|
* the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
|
|
@@ -241,12 +236,12 @@ function unparenthesized(expression: Expression): Expression {
|
|
|
241
236
|
* carries. Whether the method is the prototype's is resolution's to say.
|
|
242
237
|
*/
|
|
243
238
|
function throughMethod(call: CallExpression): MethodForm | undefined {
|
|
244
|
-
const expression =
|
|
239
|
+
const expression = parenthesesRemoved(call.expression);
|
|
245
240
|
if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
|
|
246
241
|
const [, ...rest] = call.arguments;
|
|
247
242
|
let passed: readonly Expression[] = rest;
|
|
248
243
|
if (expression.name.text === "apply") {
|
|
249
|
-
const list = rest[0] === undefined ? undefined :
|
|
244
|
+
const list = rest[0] === undefined ? undefined : parenthesesRemoved(rest[0]);
|
|
250
245
|
passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
|
|
251
246
|
}
|
|
252
247
|
return {
|
|
@@ -255,7 +250,7 @@ function throughMethod(call: CallExpression): MethodForm | undefined {
|
|
|
255
250
|
};
|
|
256
251
|
}
|
|
257
252
|
if (isCallExpression(expression)) {
|
|
258
|
-
const bound =
|
|
253
|
+
const bound = parenthesesRemoved(expression.expression);
|
|
259
254
|
if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
|
|
260
255
|
return {
|
|
261
256
|
method: bound.name,
|
package/src/session.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type Queries,
|
|
27
27
|
type UnansweredQuestion,
|
|
28
28
|
} from "./query.ts";
|
|
29
|
+
import { MANIFEST } from "./fs/manifest.ts";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The compiler the analysis reads through: a client over a protocol to a separate
|
|
@@ -115,7 +116,7 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
115
116
|
if (components !== undefined && components.extensions.length > 0) {
|
|
116
117
|
const made = components.host.temporaryDirectory();
|
|
117
118
|
try {
|
|
118
|
-
components.host.writeDocument(joinPath(made.path,
|
|
119
|
+
components.host.writeDocument(joinPath(made.path, MANIFEST), [
|
|
119
120
|
mapperManifest(components.host.componentMapperCommand()),
|
|
120
121
|
]);
|
|
121
122
|
} catch (error: unknown) {
|
package/src/suppress-file.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Host } from "./host.ts";
|
|
|
10
10
|
import type { InventorySymbol } from "./inventory.ts";
|
|
11
11
|
import { jsonChunks } from "./json-chunks.ts";
|
|
12
12
|
import { joinPath } from "./paths.ts";
|
|
13
|
-
import { positionAt, walkDocument } from "./json-document.ts";
|
|
13
|
+
import { isRecord, positionAt, walkDocument } from "./json-document.ts";
|
|
14
14
|
import type { Position } from "./position.ts";
|
|
15
15
|
import { declarationKey } from "./ref.ts";
|
|
16
16
|
import {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type SuppressionRecord,
|
|
27
27
|
type Suppressions,
|
|
28
28
|
} from "./suppress.ts";
|
|
29
|
+
import { message } from "./product.ts";
|
|
29
30
|
|
|
30
31
|
/** The name and the location the grammar fixes for the ignore file. */
|
|
31
32
|
export const IGNORE_FILE = "deadset-ignore.json";
|
|
@@ -79,10 +80,6 @@ function documentSite(shape: Shape): Position {
|
|
|
79
80
|
return { path: shape.file, line: 0, column: 0 };
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
83
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
83
|
/** What one entry or row resolves to. */
|
|
87
84
|
interface Read {
|
|
88
85
|
readonly records: readonly SuppressionRecord[];
|
|
@@ -100,7 +97,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
100
97
|
const refuse = (text: string, want: string): never => {
|
|
101
98
|
throw new SuppressionError(shape.mechanism, siteText(site), text, want);
|
|
102
99
|
};
|
|
103
|
-
if (!
|
|
100
|
+
if (!isRecord(value)) {
|
|
104
101
|
return refuse(JSON.stringify(value), shape.recordWant);
|
|
105
102
|
}
|
|
106
103
|
for (const [key, held] of Object.entries(value)) {
|
|
@@ -200,9 +197,9 @@ function readDocument(
|
|
|
200
197
|
try {
|
|
201
198
|
parsed = JSON.parse(text);
|
|
202
199
|
} catch (error: unknown) {
|
|
203
|
-
return malformed(`${shape.file}: ${
|
|
200
|
+
return malformed(`${shape.file}: ${message(error)}`);
|
|
204
201
|
}
|
|
205
|
-
if (!
|
|
202
|
+
if (!isRecord(parsed)) {
|
|
206
203
|
return malformed(shape.file);
|
|
207
204
|
}
|
|
208
205
|
const walk = walkDocument(text, shape.array);
|
|
@@ -328,9 +325,9 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
|
|
|
328
325
|
try {
|
|
329
326
|
parsed = JSON.parse(text);
|
|
330
327
|
} catch (error: unknown) {
|
|
331
|
-
return refuse(
|
|
328
|
+
return refuse(message(error));
|
|
332
329
|
}
|
|
333
|
-
const held =
|
|
330
|
+
const held = isRecord(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
|
|
334
331
|
if (!Array.isArray(held)) {
|
|
335
332
|
return refuse(BASELINE_SHAPE.documentWant);
|
|
336
333
|
}
|
|
@@ -338,7 +335,7 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
|
|
|
338
335
|
try {
|
|
339
336
|
readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
|
|
340
337
|
} catch (error: unknown) {
|
|
341
|
-
return refuse(
|
|
338
|
+
return refuse(message(error));
|
|
342
339
|
}
|
|
343
340
|
// readRecord has checked every member is a string and the code and symbol are present.
|
|
344
341
|
const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
|
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
|
+
}
|