@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
package/src/enum-group.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
isIdentifier,
|
|
19
19
|
isMethodDeclaration,
|
|
20
20
|
isNoSubstitutionTemplateLiteral,
|
|
21
|
-
isParenthesizedExpression,
|
|
22
21
|
isParenthesizedTypeNode,
|
|
23
22
|
isPropertyAccessExpression,
|
|
24
23
|
isReturnStatement,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
type Node,
|
|
29
28
|
type SourceFile,
|
|
30
29
|
} from "@typescript/native/ast";
|
|
30
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
31
31
|
import { TypeFlags, type Type } from "@typescript/native/sync";
|
|
32
32
|
import { aliasChains } from "./alias-chain.ts";
|
|
33
33
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
@@ -55,15 +55,6 @@ interface Site {
|
|
|
55
55
|
readonly at: Node;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** The expression one node is, through however many parentheses are written around it. */
|
|
59
|
-
function unparenthesized(node: Node): Node {
|
|
60
|
-
let held = node;
|
|
61
|
-
while (isParenthesizedExpression(held)) {
|
|
62
|
-
held = held.expression;
|
|
63
|
-
}
|
|
64
|
-
return held;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
/**
|
|
68
59
|
* The name node a type node names its declaration by: the identifier of a type
|
|
69
60
|
* reference, the right side of a qualified one, through parentheses. A type that is no
|
|
@@ -86,7 +77,7 @@ function typeName(type: Node | undefined): Node | undefined {
|
|
|
86
77
|
|
|
87
78
|
/** The name node an expression names an object by: an identifier, or a property access's name. */
|
|
88
79
|
function objectName(expression: Node): Node | undefined {
|
|
89
|
-
const held =
|
|
80
|
+
const held = parenthesesRemoved(expression);
|
|
90
81
|
if (isIdentifier(held)) {
|
|
91
82
|
return held;
|
|
92
83
|
}
|
package/src/erased-stores.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
isNoSubstitutionTemplateLiteral,
|
|
17
17
|
isNumericLiteral,
|
|
18
18
|
isObjectLiteralExpression,
|
|
19
|
-
isParenthesizedExpression,
|
|
20
19
|
isPropertyAccessExpression,
|
|
21
20
|
isPropertyAssignment,
|
|
22
21
|
isShorthandPropertyAssignment,
|
|
@@ -31,6 +30,7 @@ import { SymbolFlags, TypeFlags, type Type } from "@typescript/native/sync";
|
|
|
31
30
|
import { must } from "./query.ts";
|
|
32
31
|
import type { ProjectView } from "./session.ts";
|
|
33
32
|
import { checkedTypes } from "./value-reach.ts";
|
|
33
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
34
34
|
|
|
35
35
|
/** The types that keep nothing of the value they are given. */
|
|
36
36
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
@@ -81,10 +81,6 @@ function literalKey(node: Expression): string | undefined {
|
|
|
81
81
|
: undefined;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function unwrapped(node: Expression): Expression {
|
|
85
|
-
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
/**
|
|
89
85
|
* Whether a variable declaration's type can differ from what is stored in it: it carries an
|
|
90
86
|
* annotation, or it has neither an annotation nor an initializer, so its type evolves.
|
|
@@ -150,8 +146,8 @@ function propertyAt(source: Expression, name: string): Expression | undefined {
|
|
|
150
146
|
* here, and a default value is stored in its target too.
|
|
151
147
|
*/
|
|
152
148
|
function patternTargets(pattern: Expression, from: Stored, out: Target[]): void {
|
|
153
|
-
const target =
|
|
154
|
-
const source = from.path.length === 0 ?
|
|
149
|
+
const target = parenthesesRemoved(pattern);
|
|
150
|
+
const source = from.path.length === 0 ? parenthesesRemoved(from.value) : from.value;
|
|
155
151
|
const into = (element: Expression, step: Step, literal: Expression | undefined): void => {
|
|
156
152
|
patternTargets(
|
|
157
153
|
element,
|
package/src/exempt-classes.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function typescriptVisibilityOf(exemptionClass: TSExemptionClass): TypeSc
|
|
|
48
48
|
return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
/** Every class of the vocabulary, whichever language it runs on, in the vocabulary's order. */
|
|
52
|
+
export const EXEMPTION_CLASS_NAMES: readonly ExemptionClass[] = EXEMPTION_CLASSES.map(
|
|
53
|
+
(row) => row.class,
|
|
54
|
+
);
|
package/src/exempt.ts
CHANGED
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { EXEMPTION_CLASSES } from "./contract/contract.generated.ts";
|
|
11
11
|
import type { Config, TSSection } from "./config.ts";
|
|
12
|
-
import { ConfigError } from "./config.ts";
|
|
13
12
|
import {
|
|
14
|
-
isExemptionClass,
|
|
15
13
|
TS_EXEMPTION_CLASSES,
|
|
16
14
|
typescriptVisibilityOf,
|
|
17
15
|
type ExemptionClass,
|
|
@@ -25,6 +23,7 @@ import { Unanswerable } from "./query.ts";
|
|
|
25
23
|
import type { ProjectView } from "./session.ts";
|
|
26
24
|
import type { Templates } from "./template-field.ts";
|
|
27
25
|
import { sweep, type Exemption, type Implementing, type Mode, type SweepInput } from "./sweep.ts";
|
|
26
|
+
import { bytewise } from "./text/compare.ts";
|
|
28
27
|
|
|
29
28
|
/** One declaration a detector holds back, and where and why. */
|
|
30
29
|
export interface Evidence {
|
|
@@ -73,24 +72,11 @@ interface Holding {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
|
-
* The classes `exemptions.disabled` switches off. A
|
|
77
|
-
*
|
|
78
|
-
* switches nothing off here, so one configuration serves every analyzer.
|
|
75
|
+
* The classes `exemptions.disabled` switches off. A class that runs on another language
|
|
76
|
+
* only switches nothing off here, so one configuration serves every analyzer.
|
|
79
77
|
*/
|
|
80
78
|
export function disabledClasses(config: Config): ReadonlySet<ExemptionClass> {
|
|
81
|
-
|
|
82
|
-
for (const name of config.exemptionsDisabled) {
|
|
83
|
-
if (!isExemptionClass(name)) {
|
|
84
|
-
const known = EXEMPTION_CLASSES.map((row) => row.class).join(", ");
|
|
85
|
-
throw new ConfigError(
|
|
86
|
-
"malformed",
|
|
87
|
-
"exemptions.disabled",
|
|
88
|
-
`exemptions.disabled: ${JSON.stringify(name)} is not an exemption class: the classes are ${known}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
disabled.add(name);
|
|
92
|
-
}
|
|
93
|
-
return disabled;
|
|
79
|
+
return new Set(config.exemptionsDisabled);
|
|
94
80
|
}
|
|
95
81
|
|
|
96
82
|
/**
|
|
@@ -221,14 +207,6 @@ export function heldRecords(records: readonly Exemption[], holding: Holding): De
|
|
|
221
207
|
return { records: exemptionsOf(found), inTestFiles };
|
|
222
208
|
}
|
|
223
209
|
|
|
224
|
-
/** Two strings ordered bytewise. */
|
|
225
|
-
function compare(a: string, b: string): number {
|
|
226
|
-
if (a === b) {
|
|
227
|
-
return 0;
|
|
228
|
-
}
|
|
229
|
-
return a < b ? -1 : 1;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
210
|
/**
|
|
233
211
|
* Records as a run holds them: ordered by site, then class, then declaration, and one
|
|
234
212
|
* per declaration, class, detail and file it holds while live, the one at the first
|
|
@@ -236,7 +214,7 @@ function compare(a: string, b: string): number {
|
|
|
236
214
|
*/
|
|
237
215
|
export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[] {
|
|
238
216
|
const ordered = [...found].sort(
|
|
239
|
-
(a, b) => byPosition(a.site, b.site) ||
|
|
217
|
+
(a, b) => byPosition(a.site, b.site) || bytewise(a.class, b.class) || bytewise(a.id, b.id),
|
|
240
218
|
);
|
|
241
219
|
const seen = new Set<string>();
|
|
242
220
|
return ordered.filter((record) => {
|
package/src/file-facts.ts
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { SourceFile } from "@typescript/native/ast";
|
|
8
|
-
import { IGNORED_DIRECTORIES } from "./discover.ts";
|
|
9
8
|
import type { Host } from "./host.ts";
|
|
10
9
|
import { packageScope } from "./inventory.ts";
|
|
11
10
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
12
11
|
import { renderRef } from "./ref.ts";
|
|
13
12
|
import type { ProjectView } from "./session.ts";
|
|
14
13
|
import type { UnseenLoad } from "./unseen-loads.ts";
|
|
14
|
+
import { filesBelow } from "./fs/tree.ts";
|
|
15
15
|
|
|
16
16
|
/** A file name the compiler reads as a source file. */
|
|
17
17
|
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
|
|
@@ -74,29 +74,11 @@ function lastLine(text: string): number {
|
|
|
74
74
|
* that cannot be read is skipped, as discovery skips it.
|
|
75
75
|
*/
|
|
76
76
|
export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
entries = host.readDirectory(joinPath(targetRoot, dir));
|
|
83
|
-
} catch {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
88
|
-
if (entry.directory) {
|
|
89
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
90
|
-
pending.push(path);
|
|
91
|
-
}
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
|
|
95
|
-
found.push(path);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return found.sort((a, b) => (a < b ? -1 : 1));
|
|
77
|
+
return filesBelow(
|
|
78
|
+
host,
|
|
79
|
+
targetRoot,
|
|
80
|
+
(name) => SOURCE_FILE.test(name) && !DECLARATION_FILE.test(name),
|
|
81
|
+
);
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
/**
|
|
@@ -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));
|
package/src/findings-pass.ts
CHANGED
|
@@ -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
|
|
|
@@ -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
|
}
|
|
@@ -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/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
|
}
|