@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.8
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 +24 -5
- package/dist/src/analysis.js +37 -56
- 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 +6 -82
- package/dist/src/configuration-files.js +27 -168
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +2 -2
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/contract.generated.js +13 -0
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/conventions.js +20 -60
- package/dist/src/dependencies.js +25 -43
- package/dist/src/discover.js +14 -40
- package/dist/src/edges.js +38 -122
- 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 +0 -4
- package/dist/src/exempt.js +6 -22
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +5 -4
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +1 -1
- package/dist/src/findings/non-code-artifacts.js +2 -2
- package/dist/src/findings/reads-and-writes.js +2 -1
- package/dist/src/findings/self-check.js +3 -2
- package/dist/src/findings/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +7 -12
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +59 -0
- 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/injection-container.js +1 -1
- package/dist/src/intra-function-parts.js +10 -13
- package/dist/src/inventory.js +2 -21
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +0 -7
- 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 +11 -79
- package/dist/src/product.js +13 -0
- package/dist/src/references.js +14 -27
- package/dist/src/report-schema.js +3 -225
- 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 +245 -724
- package/dist/src/roots.js +7 -17
- package/dist/src/run.js +55 -61
- package/dist/src/schema.js +320 -343
- package/dist/src/scope.js +34 -103
- package/dist/src/serialization-contract.js +5 -9
- package/dist/src/session.js +2 -1
- package/dist/src/suppress-file.js +21 -25
- package/dist/src/suppress.js +3 -3
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +9 -19
- 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/usage.js +10 -0
- package/dist/src/verbs/analyze.js +25 -26
- package/dist/src/verbs/describe.js +4 -4
- package/dist/src/verbs/explain.js +8 -6
- package/dist/src/verbs/print-projects.js +4 -3
- 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 +55 -76
- 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 +31 -296
- package/src/configuration-files.ts +27 -170
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +4 -4
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/contract.generated.ts +25 -0
- package/src/contract/report.generated.ts +3 -2
- package/src/conventions.ts +26 -74
- package/src/dependencies.ts +32 -50
- package/src/discover.ts +15 -40
- package/src/edges.ts +41 -140
- 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 +0 -5
- package/src/exempt.ts +8 -30
- package/src/file-facts.ts +6 -24
- package/src/findings/completion.ts +5 -6
- package/src/findings/emitter.ts +2 -2
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +1 -1
- package/src/findings/non-code-artifacts.ts +4 -4
- package/src/findings/reads-and-writes.ts +2 -1
- package/src/findings/self-check.ts +3 -2
- package/src/findings/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +9 -15
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +74 -0
- package/src/fs/manifest.ts +52 -0
- package/src/fs/tree.ts +110 -0
- package/src/git-ignore.ts +38 -25
- package/src/host.ts +12 -5
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/injection-container.ts +1 -1
- package/src/intra-function-parts.ts +10 -14
- package/src/inventory.ts +2 -20
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +0 -8
- 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 +12 -92
- package/src/product.ts +18 -0
- package/src/references.ts +14 -29
- package/src/report-schema.ts +3 -301
- package/src/report.ts +24 -34
- package/src/reporters/sarif.ts +4 -4
- package/src/reporters/template.ts +4 -20
- package/src/resolve.ts +283 -1114
- package/src/roots.ts +7 -20
- package/src/run.ts +57 -75
- package/src/schema.ts +391 -388
- package/src/scope.ts +40 -126
- package/src/serialization-contract.ts +5 -10
- package/src/session.ts +2 -1
- package/src/suppress-file.ts +21 -29
- package/src/suppress.ts +3 -3
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +15 -25
- 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/usage.ts +10 -0
- package/src/verbs/analyze.ts +32 -42
- package/src/verbs/describe.ts +4 -4
- package/src/verbs/explain.ts +8 -10
- package/src/verbs/print-projects.ts +4 -3
- package/src/verbs/print-retained.ts +2 -1
- package/src/verbs/print-roots.ts +2 -1
- package/src/verbs/verb.ts +2 -2
- 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
|
@@ -5,6 +5,7 @@ import type { MatrixCandidate } from "../matrix.ts";
|
|
|
5
5
|
import { positionKey } from "../position.ts";
|
|
6
6
|
import { declarationKey } from "../ref.ts";
|
|
7
7
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
8
|
+
import { bytewise } from "../text/compare.ts";
|
|
8
9
|
|
|
9
10
|
/** The codes this emitter reports. */
|
|
10
11
|
type ReadsAndWritesCode =
|
|
@@ -123,7 +124,7 @@ function writeOnlyMessage(
|
|
|
123
124
|
: "never read";
|
|
124
125
|
const cascade = [
|
|
125
126
|
...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
126
|
-
...[...held.alone].sort().map((name) => `import ${name}`),
|
|
127
|
+
...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
|
|
127
128
|
];
|
|
128
129
|
const word = SUBJECT_WORDS[symbol.kind] ?? "member";
|
|
129
130
|
const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SuppressionRecord,
|
|
13
13
|
} from "../suppress.ts";
|
|
14
14
|
import type { Emitter } from "./emitter.ts";
|
|
15
|
+
import { bytewise } from "../text/compare.ts";
|
|
15
16
|
|
|
16
17
|
/** The subject kind of a finding about a configured declaration. */
|
|
17
18
|
const DECLARATION_SUBJECT = "configured-declaration";
|
|
@@ -171,10 +172,10 @@ export function staleSuppressions(
|
|
|
171
172
|
bySite.set(key, held);
|
|
172
173
|
}
|
|
173
174
|
return [...bySite.values()].map(({ first, records }) => {
|
|
174
|
-
const codes = [...new Set(records.map((one) => one.record.code))].sort();
|
|
175
|
+
const codes = [...new Set(records.map((one) => one.record.code))].sort(bytewise);
|
|
175
176
|
const reports = [...new Set(records.flatMap((one) => one.reports))]
|
|
176
177
|
.filter((code) => !codes.includes(code))
|
|
177
|
-
.sort();
|
|
178
|
+
.sort(bytewise);
|
|
178
179
|
const rows = codes.filter(isUnsuppressibleCode);
|
|
179
180
|
const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
|
|
180
181
|
const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
|
|
@@ -218,7 +218,7 @@ export const unusedDeclarations: Emitter = (input) => [
|
|
|
218
218
|
*/
|
|
219
219
|
function unusedImports(input: EmitterInput): Finding[] {
|
|
220
220
|
const generated =
|
|
221
|
-
input.config.analysis.
|
|
221
|
+
input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
|
|
222
222
|
return input.intraFunction.imports.flatMap((binding) =>
|
|
223
223
|
binding.used || generated?.has(binding.position.path) === true
|
|
224
224
|
? []
|
|
@@ -266,13 +266,13 @@ function unreachable(
|
|
|
266
266
|
export function narrowings(input: EmitterInput): readonly Finding[] {
|
|
267
267
|
const facts = factsOf(input.swept.matrix.union);
|
|
268
268
|
const { declared, loaded } = input.boundary.consumers;
|
|
269
|
-
const closed = input.config.
|
|
269
|
+
const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
|
|
270
270
|
const world = { closed, writeOnly: writeOnlyDeclarations(input) };
|
|
271
271
|
const candidates = new Map(
|
|
272
272
|
input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
|
|
273
273
|
);
|
|
274
274
|
const generated =
|
|
275
|
-
input.config.analysis.
|
|
275
|
+
input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
|
|
276
276
|
|
|
277
277
|
const findings: Finding[] = [];
|
|
278
278
|
facts.graph.symbols.forEach((symbol, at) => {
|
package/src/findings-pass.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.generated.ts";
|
|
8
8
|
import type { AnalysisInputs } from "./analysis.ts";
|
|
9
|
-
import { fails, type
|
|
9
|
+
import { fails, type ResolvedConfig } from "./config.ts";
|
|
10
10
|
import type { EdgeEvaluation } from "./edges.ts";
|
|
11
11
|
import type { CompletedFinding, Finding } from "./finding.ts";
|
|
12
12
|
import { decidedFindings, type Withheld } from "./findings/emitters.ts";
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
type Ledger,
|
|
25
25
|
} from "./suppress.ts";
|
|
26
26
|
import type { Provenance, Recorded } from "./suppress-file.ts";
|
|
27
|
+
import { bytewise } from "./text/compare.ts";
|
|
28
|
+
import { PRODUCT } from "./product.ts";
|
|
27
29
|
|
|
28
30
|
/** The counts a report's totals carry beside its findings. */
|
|
29
31
|
interface PassTotals {
|
|
@@ -49,22 +51,14 @@ export interface PassResult {
|
|
|
49
51
|
readonly ledger: Ledger;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** Two strings ordered bytewise. */
|
|
53
|
-
function compare(a: string, b: string): number {
|
|
54
|
-
if (a === b) {
|
|
55
|
-
return 0;
|
|
56
|
-
}
|
|
57
|
-
return a < b ? -1 : 1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
54
|
/** The canonical order: path, line, column, code, then the symbol reference. */
|
|
61
55
|
function canonical(a: Finding, b: Finding): number {
|
|
62
56
|
return (
|
|
63
|
-
|
|
57
|
+
bytewise(a.position.path, b.position.path) ||
|
|
64
58
|
a.position.line - b.position.line ||
|
|
65
59
|
a.position.column - b.position.column ||
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
bytewise(a.code, b.code) ||
|
|
61
|
+
bytewise(a.symbol.ref, b.symbol.ref)
|
|
68
62
|
);
|
|
69
63
|
}
|
|
70
64
|
|
|
@@ -115,7 +109,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
|
|
|
115
109
|
* suppression is stale or a finding's severity reaches the configured failing severity;
|
|
116
110
|
* clean otherwise.
|
|
117
111
|
*/
|
|
118
|
-
export function verdictOf(result: PassResult, config:
|
|
112
|
+
export function verdictOf(result: PassResult, config: ResolvedConfig): number {
|
|
119
113
|
if (result.totals.pending > 0) {
|
|
120
114
|
return EXIT_PENDING;
|
|
121
115
|
}
|
|
@@ -123,7 +117,7 @@ export function verdictOf(result: PassResult, config: Config): number {
|
|
|
123
117
|
return EXIT_FINDINGS;
|
|
124
118
|
}
|
|
125
119
|
const failing = result.findings.some((finding) =>
|
|
126
|
-
fails(finding.severity, config.reporters.
|
|
120
|
+
fails(finding.severity, config.reporters.fail_on),
|
|
127
121
|
);
|
|
128
122
|
return failing ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
129
123
|
}
|
|
@@ -161,5 +155,5 @@ export function rowKey(row: Recorded): string {
|
|
|
161
155
|
|
|
162
156
|
/** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
|
|
163
157
|
export function analyzerProvenance(version: string): Provenance {
|
|
164
|
-
return { analyzer:
|
|
158
|
+
return { analyzer: PRODUCT, version };
|
|
165
159
|
}
|
|
@@ -55,8 +55,8 @@ function resolveContracts<Brand>(
|
|
|
55
55
|
contracts: readonly LifecycleContract[],
|
|
56
56
|
): readonly ResolvedContract[] {
|
|
57
57
|
const entries: DeclarationEntry[] = contracts.flatMap((contract) => [
|
|
58
|
-
...contract.components,
|
|
59
|
-
...contract.bases,
|
|
58
|
+
...(contract.components ?? []),
|
|
59
|
+
...(contract.bases ?? []),
|
|
60
60
|
]);
|
|
61
61
|
const resolved = resolveEntries(project, held, entries);
|
|
62
62
|
let next = 0;
|
|
@@ -67,8 +67,8 @@ function resolveContracts<Brand>(
|
|
|
67
67
|
};
|
|
68
68
|
return contracts.map((contract) => ({
|
|
69
69
|
contract,
|
|
70
|
-
components: take(contract.components
|
|
71
|
-
bases: take(contract.bases
|
|
70
|
+
components: take(contract.components?.length ?? 0),
|
|
71
|
+
bases: take(contract.bases?.length ?? 0),
|
|
72
72
|
}));
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -243,11 +243,11 @@ function byBases<Brand>(
|
|
|
243
243
|
*/
|
|
244
244
|
export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
245
245
|
const { project, held, targetRoot } = input;
|
|
246
|
-
if (input.ts.
|
|
246
|
+
if (input.ts.lifecycle_contracts.length === 0) {
|
|
247
247
|
return [];
|
|
248
248
|
}
|
|
249
249
|
const chains = aliasChains(project, held);
|
|
250
|
-
const contracts = resolveContracts(project, held, input.ts.
|
|
250
|
+
const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
|
|
251
251
|
const components = [
|
|
252
252
|
...byComponents(project, held, chains, contracts),
|
|
253
253
|
...byBases(project, held, chains, contracts),
|
|
@@ -256,7 +256,7 @@ export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly
|
|
|
256
256
|
return [];
|
|
257
257
|
}
|
|
258
258
|
const ids = new Set(components.map((one) => one.id));
|
|
259
|
-
const sides: readonly ClassSide[] = input.ts.
|
|
259
|
+
const sides: readonly ClassSide[] = input.ts.lifecycle_contracts.some((contract) =>
|
|
260
260
|
contract.members.some((name) => name.endsWith(":static")),
|
|
261
261
|
)
|
|
262
262
|
? ["instance", "static"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one read of a document the target holds at its root, which a checkout under analysis
|
|
3
|
+
* controls: the repository configuration, the ignore file, the baseline and the edges
|
|
4
|
+
* document. The read stays inside the target root, takes a regular file alone, and holds no
|
|
5
|
+
* more of it than the Contract's document bound.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CONFIG_MAX_BYTES } from "../contract/config.generated.ts";
|
|
9
|
+
import type { Host } from "../host.ts";
|
|
10
|
+
import { joinPath, relativePath } from "../paths.ts";
|
|
11
|
+
import { message } from "../product.ts";
|
|
12
|
+
import { utf8Bytes } from "../reporters/sha256.ts";
|
|
13
|
+
import { UsageError } from "../usage.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A document at the target root that is not one to read: its name resolves outside the
|
|
17
|
+
* target root or to anything but a regular file, or the read fails. Nothing is read from
|
|
18
|
+
* it, and the run ends with the failure code.
|
|
19
|
+
*/
|
|
20
|
+
export class TargetDocumentError extends Error {
|
|
21
|
+
constructor(label: string, reason: string) {
|
|
22
|
+
super(`${label}: ${reason}`);
|
|
23
|
+
this.name = "TargetDocumentError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
|
|
28
|
+
export function refuseOverBound(label: string, text: string): void {
|
|
29
|
+
if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
|
|
30
|
+
throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What `read` returns, or its throw as the document's {@link TargetDocumentError}. */
|
|
35
|
+
function attempt<T>(label: string, read: () => T): T {
|
|
36
|
+
try {
|
|
37
|
+
return read();
|
|
38
|
+
} catch (error: unknown) {
|
|
39
|
+
throw new TargetDocumentError(label, message(error));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
|
|
45
|
+
* there, each refusal naming it `label`. A document past the bound is read one byte past
|
|
46
|
+
* it and refused as {@link refuseOverBound} refuses it.
|
|
47
|
+
*/
|
|
48
|
+
export function readTargetDocument(
|
|
49
|
+
host: Host,
|
|
50
|
+
targetRoot: string,
|
|
51
|
+
name: string,
|
|
52
|
+
label = name,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
const path = joinPath(targetRoot, name);
|
|
55
|
+
const kind = attempt(label, () => host.kindOf(path));
|
|
56
|
+
if (kind === "absent") {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const real = attempt(label, () => host.realPath(path));
|
|
60
|
+
if (
|
|
61
|
+
relativePath(
|
|
62
|
+
attempt(label, () => host.realPath(targetRoot)),
|
|
63
|
+
real,
|
|
64
|
+
) === undefined
|
|
65
|
+
) {
|
|
66
|
+
throw new TargetDocumentError(label, `resolves to ${real}, outside the target root`);
|
|
67
|
+
}
|
|
68
|
+
if (kind !== "file") {
|
|
69
|
+
throw new TargetDocumentError(label, "is not a regular file");
|
|
70
|
+
}
|
|
71
|
+
const text = attempt(label, () => host.readFile(real, CONFIG_MAX_BYTES + 1));
|
|
72
|
+
refuseOverBound(label, text);
|
|
73
|
+
return text;
|
|
74
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Host } from "../host.ts";
|
|
2
|
+
import { isRecord } from "../json-document.ts";
|
|
3
|
+
import { joinPath } from "../paths.ts";
|
|
4
|
+
|
|
5
|
+
/** The file a package's manifest is read from. */
|
|
6
|
+
export const MANIFEST = "package.json";
|
|
7
|
+
|
|
8
|
+
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
9
|
+
export const INSTALLED_PACKAGES = "node_modules";
|
|
10
|
+
|
|
11
|
+
/** Whether one path passes through an installed-packages directory. */
|
|
12
|
+
export function inInstalledPackages(path: string): boolean {
|
|
13
|
+
return path.split("/").includes(INSTALLED_PACKAGES);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One JSON file's text and decoded value, or undefined where it cannot be read or parsed. */
|
|
17
|
+
export function readDocument(
|
|
18
|
+
host: Host,
|
|
19
|
+
path: string,
|
|
20
|
+
): { text: string; value: unknown } | undefined {
|
|
21
|
+
let text: string;
|
|
22
|
+
try {
|
|
23
|
+
text = host.readFile(path);
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return { text, value: JSON.parse(text) as unknown };
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One JSON file's decoded value, or undefined where it cannot be read or parsed. */
|
|
35
|
+
export function readJSON(host: Host, path: string): unknown {
|
|
36
|
+
return readDocument(host, path)?.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The manifest in one directory, decoded, or undefined where none reads as an object. A
|
|
41
|
+
* manifest this program cannot read declares nothing, the same answer as an absent one:
|
|
42
|
+
* the package manager owns the refusal of a manifest it rejects.
|
|
43
|
+
*/
|
|
44
|
+
export function manifestAt(host: Host, dir: string): Record<string, unknown> | undefined {
|
|
45
|
+
const value = readJSON(host, joinPath(dir, MANIFEST));
|
|
46
|
+
return isRecord(value) ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether one directory holds a manifest file. */
|
|
50
|
+
export function holdsManifest(host: Host, dir: string): boolean {
|
|
51
|
+
return host.kindOf(joinPath(dir, MANIFEST)) === "file";
|
|
52
|
+
}
|
package/src/fs/tree.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { DirectoryEntry, Host } from "../host.ts";
|
|
2
|
+
import { ancestors, joinPath, resolvePath } from "../paths.ts";
|
|
3
|
+
import { LANGUAGE_ROWS } from "../contract/contract.generated.ts";
|
|
4
|
+
import { LANGUAGE } from "../product.ts";
|
|
5
|
+
import { bytewise } from "../text/compare.ts";
|
|
6
|
+
import { globExpression } from "../text/glob.ts";
|
|
7
|
+
|
|
8
|
+
/** The directories no walk of the tree enters, by the patterns the language's row names. */
|
|
9
|
+
const SKIPPED_DIRECTORIES = LANGUAGE_ROWS[LANGUAGE].skippedDirectories.map(globExpression);
|
|
10
|
+
|
|
11
|
+
function skipped(name: string): boolean {
|
|
12
|
+
return SKIPPED_DIRECTORIES.some((pattern) => pattern.test(name));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A git repository's working tree root, and the `info/exclude` file git reads for it. */
|
|
16
|
+
interface Repository {
|
|
17
|
+
readonly root: string;
|
|
18
|
+
/** Undefined where a `.git` file names no repository directory. */
|
|
19
|
+
readonly exclude: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One file's text without its line ending, or undefined where it cannot be read. */
|
|
23
|
+
function lineOf(host: Host, path: string): string | undefined {
|
|
24
|
+
try {
|
|
25
|
+
return host.readFile(path).replace(/\r?\n$/u, "");
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The repository whose working tree holds `dir`: the nearest directory at or above it with a
|
|
33
|
+
* `.git`. A `.git` file, which a linked worktree or a submodule holds, names the repository's
|
|
34
|
+
* directory on its `gitdir:` line; that directory's `commondir` file, where it has one, names
|
|
35
|
+
* the directory shared with the main worktree, which holds `info/exclude`:
|
|
36
|
+
* https://git-scm.com/docs/gitrepository-layout
|
|
37
|
+
*/
|
|
38
|
+
export function repositoryOf(host: Host, dir: string): Repository | undefined {
|
|
39
|
+
for (const root of ancestors(dir)) {
|
|
40
|
+
const dotGit = joinPath(root, ".git");
|
|
41
|
+
const kind = host.kindOf(dotGit);
|
|
42
|
+
if (kind === "directory") {
|
|
43
|
+
return { root, exclude: joinPath(dotGit, "info/exclude") };
|
|
44
|
+
}
|
|
45
|
+
if (kind === "file") {
|
|
46
|
+
const named = /^gitdir: (.+)$/u.exec(lineOf(host, dotGit) ?? "")?.[1];
|
|
47
|
+
const gitDir = named === undefined ? undefined : resolvePath(root, named);
|
|
48
|
+
const common = gitDir === undefined ? undefined : lineOf(host, joinPath(gitDir, "commondir"));
|
|
49
|
+
const shared =
|
|
50
|
+
gitDir === undefined || common === undefined ? gitDir : resolvePath(gitDir, common);
|
|
51
|
+
return { root, exclude: shared === undefined ? undefined : joinPath(shared, "info/exclude") };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The entries of one directory, none where it cannot be read. */
|
|
58
|
+
export function entriesOf(host: Host, dir: string): readonly DirectoryEntry[] {
|
|
59
|
+
try {
|
|
60
|
+
return host.readDirectory(dir);
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The names of the files directly in `dir` whose name `accepts`, in bytewise order. */
|
|
67
|
+
export function filesIn(
|
|
68
|
+
host: Host,
|
|
69
|
+
dir: string,
|
|
70
|
+
accepts: (name: string) => boolean,
|
|
71
|
+
): readonly string[] {
|
|
72
|
+
return entriesOf(host, dir)
|
|
73
|
+
.filter((entry) => !entry.directory && accepts(entry.name))
|
|
74
|
+
.map((entry) => entry.name)
|
|
75
|
+
.sort(bytewise);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The names of the directories directly in `dir` a walk enters, in bytewise order. */
|
|
79
|
+
export function directoriesIn(host: Host, dir: string): readonly string[] {
|
|
80
|
+
return entriesOf(host, dir)
|
|
81
|
+
.filter((entry) => entry.directory && !skipped(entry.name))
|
|
82
|
+
.map((entry) => entry.name)
|
|
83
|
+
.sort(bytewise);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every file below `root` whose name `accepts`, by its path relative to `root` in bytewise
|
|
88
|
+
* order, read in no skipped directory.
|
|
89
|
+
*/
|
|
90
|
+
export function filesBelow(
|
|
91
|
+
host: Host,
|
|
92
|
+
root: string,
|
|
93
|
+
accepts: (name: string) => boolean,
|
|
94
|
+
): readonly string[] {
|
|
95
|
+
const found: string[] = [];
|
|
96
|
+
const pending = [""];
|
|
97
|
+
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
98
|
+
for (const entry of entriesOf(host, joinPath(root, dir))) {
|
|
99
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
100
|
+
if (!entry.directory) {
|
|
101
|
+
if (accepts(entry.name)) {
|
|
102
|
+
found.push(path);
|
|
103
|
+
}
|
|
104
|
+
} else if (!skipped(entry.name)) {
|
|
105
|
+
pending.push(path);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return found.sort(bytewise);
|
|
110
|
+
}
|
package/src/git-ignore.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which files
|
|
2
|
+
* Which files git ignores below a tree's root, read as git reads its ignore files:
|
|
3
3
|
* https://git-scm.com/docs/gitignore#_pattern_format
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { globExpression } from "./glob.ts";
|
|
7
6
|
import type { Host } from "./host.ts";
|
|
8
|
-
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
7
|
+
import { ancestors, dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
8
|
+
import { gitignoreExpression } from "./text/glob.ts";
|
|
9
|
+
import { repositoryOf } from "./fs/tree.ts";
|
|
9
10
|
|
|
10
|
-
/** One pattern line of
|
|
11
|
+
/** One pattern line of an ignore file. */
|
|
11
12
|
interface Rule {
|
|
12
13
|
readonly negated: boolean;
|
|
13
14
|
/** Whether the pattern ends with `/`, so it matches a directory alone. */
|
|
@@ -16,7 +17,7 @@ interface Rule {
|
|
|
16
17
|
readonly expression: RegExp;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/** The rules one
|
|
20
|
+
/** The rules one ignore file's text writes, in order. */
|
|
20
21
|
function rulesOf(text: string): readonly Rule[] {
|
|
21
22
|
const rules: Rule[] = [];
|
|
22
23
|
for (const raw of text.split(/\r?\n/u)) {
|
|
@@ -25,7 +26,7 @@ function rulesOf(text: string): readonly Rule[] {
|
|
|
25
26
|
continue;
|
|
26
27
|
}
|
|
27
28
|
const negated = line.startsWith("!");
|
|
28
|
-
line = negated
|
|
29
|
+
line = negated ? line.slice(1) : line;
|
|
29
30
|
const directoryOnly = line.endsWith("/");
|
|
30
31
|
line = line.replace(/\/+$/u, "");
|
|
31
32
|
if (line === "") {
|
|
@@ -38,43 +39,55 @@ function rulesOf(text: string): readonly Rule[] {
|
|
|
38
39
|
rules.push({
|
|
39
40
|
negated,
|
|
40
41
|
directoryOnly,
|
|
41
|
-
expression:
|
|
42
|
+
expression: gitignoreExpression(anchored ? pattern : `**/${pattern}`),
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
return rules;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function rulesIn(host: Host, path: string): readonly Rule[] {
|
|
49
|
+
try {
|
|
50
|
+
return rulesOf(host.readFile(path));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
|
-
* Whether
|
|
49
|
-
*
|
|
50
|
-
*
|
|
57
|
+
* Whether git ignores one absolute path below `root`: by the repository's `info/exclude`,
|
|
58
|
+
* then the `.gitignore` files from the repository's root down to the path's directory, a
|
|
59
|
+
* later pattern overriding an earlier one. A path outside `root` is not ignored, and a file
|
|
60
|
+
* below an ignored directory stays ignored whatever a later pattern says, as git cannot
|
|
61
|
+
* re-include it. With no repository at or above `root`, the files at and below it are read;
|
|
62
|
+
* with one, the directories above `root` give patterns but are not themselves tested.
|
|
51
63
|
*/
|
|
52
64
|
export function gitIgnored(host: Host, root: string): (path: string) => boolean {
|
|
65
|
+
const found = repositoryOf(host, root);
|
|
66
|
+
const repository = found?.root;
|
|
67
|
+
const above =
|
|
68
|
+
repository === undefined ? [] : [...ancestors(dirnamePath(root), repository)].reverse();
|
|
69
|
+
const excluded = found?.exclude === undefined ? [] : rulesIn(host, found.exclude);
|
|
53
70
|
const read = new Map<string, readonly Rule[]>();
|
|
54
71
|
const rulesAt = (dir: string): readonly Rule[] => {
|
|
55
72
|
let rules = read.get(dir);
|
|
56
73
|
if (rules === undefined) {
|
|
57
|
-
|
|
58
|
-
rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
|
|
59
|
-
} catch {
|
|
60
|
-
rules = [];
|
|
61
|
-
}
|
|
74
|
+
rules = rulesIn(host, joinPath(dir, ".gitignore"));
|
|
62
75
|
read.set(dir, rules);
|
|
63
76
|
}
|
|
64
77
|
return rules;
|
|
65
78
|
};
|
|
66
|
-
/** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
|
|
67
79
|
const decided = (path: string, directory: boolean): boolean => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
const sources: [string, readonly Rule[]][] = [
|
|
81
|
+
...(repository === undefined ? [] : [[repository, excluded] as [string, readonly Rule[]]]),
|
|
82
|
+
...[...above, ...[...ancestors(dirnamePath(path), root)].reverse()].map(
|
|
83
|
+
(dir): [string, readonly Rule[]] => [dir, rulesAt(dir)],
|
|
84
|
+
),
|
|
85
|
+
];
|
|
73
86
|
let ignored = false;
|
|
74
|
-
for (const dir of
|
|
75
|
-
const below = relativePath(dir, path);
|
|
76
|
-
for (const rule of
|
|
77
|
-
if ((directory || !rule.directoryOnly) && rule.expression.test(below
|
|
87
|
+
for (const [dir, rules] of sources) {
|
|
88
|
+
const below = relativePath(dir, path) ?? "";
|
|
89
|
+
for (const rule of rules) {
|
|
90
|
+
if ((directory || !rule.directoryOnly) && rule.expression.test(below)) {
|
|
78
91
|
ignored = !rule.negated;
|
|
79
92
|
}
|
|
80
93
|
}
|
package/src/host.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** What one path names. */
|
|
2
|
-
export type PathKind = "file" | "directory" | "absent";
|
|
1
|
+
/** What one path names: `"other"` is anything else there, a named pipe, a device or a socket. */
|
|
2
|
+
export type PathKind = "file" | "directory" | "other" | "absent";
|
|
3
3
|
|
|
4
4
|
/** One entry of a directory. */
|
|
5
5
|
export interface DirectoryEntry {
|
|
@@ -17,11 +17,18 @@ export interface DirectoryEntry {
|
|
|
17
17
|
export interface Host {
|
|
18
18
|
/** The directory a relative path resolves against. */
|
|
19
19
|
workingDirectory(): string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The text of one file, or a throw naming why it cannot be read. Given `limit`, the
|
|
22
|
+
* text of no more than the file's first `limit` bytes, so a caller bounding a document
|
|
23
|
+
* asks for one byte past its bound and never holds a larger file whole.
|
|
24
|
+
*/
|
|
25
|
+
readFile(path: string, limit?: number): string;
|
|
22
26
|
/** The entries of one directory, or a throw naming why it cannot be read. */
|
|
23
27
|
readDirectory(path: string): readonly DirectoryEntry[];
|
|
24
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* What one path names once every symbolic link along it is followed, `"absent"` where
|
|
30
|
+
* nothing is there, or a throw naming why it cannot be known, as for a link loop.
|
|
31
|
+
*/
|
|
25
32
|
kindOf(path: string): PathKind;
|
|
26
33
|
/**
|
|
27
34
|
* The path one path names once every symbolic link along it is followed, or a throw
|
package/src/html-entries.ts
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
} from "@typescript/native/ast";
|
|
13
13
|
import { resolveRelativeSpecifier, type ParseFile } from "./configuration-files.ts";
|
|
14
14
|
import type { Host } from "./host.ts";
|
|
15
|
-
import { dirnamePath, joinPath,
|
|
15
|
+
import { ancestors, dirnamePath, joinPath, resolvePath } from "./paths.ts";
|
|
16
16
|
import type { SourceFiles } from "./source-files.ts";
|
|
17
|
+
import { filesBelow } from "./fs/tree.ts";
|
|
18
|
+
import { holdsManifest } from "./fs/manifest.ts";
|
|
17
19
|
|
|
18
20
|
/** One file a script loads, and the value or specifier that named it. */
|
|
19
21
|
interface HTMLEntry {
|
|
@@ -21,9 +23,6 @@ interface HTMLEntry {
|
|
|
21
23
|
readonly source: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
/** The directory no HTML file is read below. */
|
|
25
|
-
const SKIPPED = "node_modules";
|
|
26
|
-
|
|
27
26
|
/** A `<script` start tag, its attribute text captured. */
|
|
28
27
|
const SCRIPT_TAG = /<script(?=[\s>/])([^>]*)>/giu;
|
|
29
28
|
|
|
@@ -42,28 +41,6 @@ const CLASSIC_TYPES: ReadonlySet<string> = new Set(["text/javascript", "applicat
|
|
|
42
41
|
/** The prefixes an inline script's specifier names a file of the target with. */
|
|
43
42
|
const FILE_PREFIXES = ["/", "./", "../"] as const;
|
|
44
43
|
|
|
45
|
-
/** Every HTML file below `dir`, outside every `node_modules` directory, by absolute path. */
|
|
46
|
-
function htmlFiles(host: Host, dir: string): readonly string[] {
|
|
47
|
-
let entries;
|
|
48
|
-
try {
|
|
49
|
-
entries = host.readDirectory(dir);
|
|
50
|
-
} catch {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
const found: string[] = [];
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
const path = joinPath(dir, entry.name);
|
|
56
|
-
if (entry.directory) {
|
|
57
|
-
if (entry.name !== SKIPPED) {
|
|
58
|
-
found.push(...htmlFiles(host, path));
|
|
59
|
-
}
|
|
60
|
-
} else if (entry.name.endsWith(".html")) {
|
|
61
|
-
found.push(path);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return found.sort();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
44
|
/** Each attribute of one start tag's attribute text, by its lowercase name. */
|
|
68
45
|
function attributes(text: string): ReadonlyMap<string, string> {
|
|
69
46
|
const found = new Map<string, string>();
|
|
@@ -127,12 +104,7 @@ function scriptValues(parse: ParseFile | undefined, path: string, text: string):
|
|
|
127
104
|
|
|
128
105
|
/** The directory of the nearest `package.json` at or above `dir`, inside `root`, else `root`. */
|
|
129
106
|
function manifestDirectory(host: Host, root: string, dir: string): string {
|
|
130
|
-
|
|
131
|
-
if (host.kindOf(joinPath(at, "package.json")) === "file") {
|
|
132
|
-
return at;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return root;
|
|
107
|
+
return ancestors(dir, root).find((at) => holdsManifest(host, at)) ?? root;
|
|
136
108
|
}
|
|
137
109
|
|
|
138
110
|
/**
|
|
@@ -150,7 +122,8 @@ export function htmlEntries(
|
|
|
150
122
|
): readonly HTMLEntry[] {
|
|
151
123
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
152
124
|
const found: HTMLEntry[] = [];
|
|
153
|
-
for (const
|
|
125
|
+
for (const below of filesBelow(host, root, (name) => name.endsWith(".html"))) {
|
|
126
|
+
const path = joinPath(root, below);
|
|
154
127
|
let text: string;
|
|
155
128
|
try {
|
|
156
129
|
text = host.readFile(path);
|
package/src/in-operands.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@typescript/native/ast";
|
|
14
14
|
import type { ProjectView } from "./session.ts";
|
|
15
15
|
import { sourceFilesOf } from "./source-files.ts";
|
|
16
|
+
import { bytewise } from "./text/compare.ts";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The text of each string literal, or template literal with no substitution, written as
|
|
@@ -41,5 +42,5 @@ export function inOperandNames<Brand>(
|
|
|
41
42
|
file.forEachChild(visit);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
return [...found].sort();
|
|
45
|
+
return [...found].sort(bytewise);
|
|
45
46
|
}
|
|
@@ -53,7 +53,7 @@ function isInjected(node: Node): boolean {
|
|
|
53
53
|
/** The classes of the target a call whose callee an entry names passes, with that call's entry. */
|
|
54
54
|
function registrations<Brand>(input: DetectorInput<Brand>): readonly Registration[] {
|
|
55
55
|
const { project, held } = input;
|
|
56
|
-
const resolved = resolveEntries(project, held, input.ts.
|
|
56
|
+
const resolved = resolveEntries(project, held, input.ts.injection_registrations);
|
|
57
57
|
if (!resolved.some(namesADeclaration)) {
|
|
58
58
|
return [];
|
|
59
59
|
}
|