@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
|
@@ -96,6 +96,7 @@ import type { Reference } from "./references.ts";
|
|
|
96
96
|
import type { ProjectView } from "./session.ts";
|
|
97
97
|
import { signatureOf, type SignatureNode } from "./signatures.ts";
|
|
98
98
|
import { spellsNoBinding } from "./spelled-names.ts";
|
|
99
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
99
100
|
|
|
100
101
|
/** The subject kind of each part this module reads. */
|
|
101
102
|
type PartKind = "parameter" | "result" | "statement" | "store" | "case";
|
|
@@ -197,11 +198,6 @@ function spanOf(file: SourceFile, root: string, node: Node): FindingPosition {
|
|
|
197
198
|
return { ...start, endLine };
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
/** The parenthesized expression's content, through every pair of parentheses. */
|
|
201
|
-
function unwrapped(node: Node): Node {
|
|
202
|
-
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
201
|
/** The node the parentheses around one expression sit in. */
|
|
206
202
|
function outerParent(node: Node): Node {
|
|
207
203
|
return isParenthesizedExpression(node.parent) ? outerParent(node.parent) : node.parent;
|
|
@@ -209,7 +205,7 @@ function outerParent(node: Node): Node {
|
|
|
209
205
|
|
|
210
206
|
/** The name a call is made through: the identifier, or the member a property access names. */
|
|
211
207
|
function calleeName(call: CallExpression): Identifier | undefined {
|
|
212
|
-
const callee =
|
|
208
|
+
const callee = parenthesesRemoved(call.expression);
|
|
213
209
|
if (isIdentifier(callee)) {
|
|
214
210
|
return callee;
|
|
215
211
|
}
|
|
@@ -276,7 +272,7 @@ function unreachableExempt(statement: Statement): boolean {
|
|
|
276
272
|
|
|
277
273
|
/** The value a case expression is a literal of, as a key two equal literals share, or none. */
|
|
278
274
|
function caseKey(expression: Node): string | undefined {
|
|
279
|
-
const node =
|
|
275
|
+
const node = parenthesesRemoved(expression);
|
|
280
276
|
if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
|
|
281
277
|
return `s:${node.text}`;
|
|
282
278
|
}
|
|
@@ -402,7 +398,7 @@ function extendedNames(files: readonly SourceFile[]): ReadonlySet<string> {
|
|
|
402
398
|
if (isTypeReferenceNode(type)) {
|
|
403
399
|
named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
404
400
|
} else {
|
|
405
|
-
const expression =
|
|
401
|
+
const expression = parenthesesRemoved(type.expression);
|
|
406
402
|
named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
407
403
|
}
|
|
408
404
|
if (isIdentifier(named)) {
|
|
@@ -455,7 +451,7 @@ function decoratorFixes(constructor: ConstructorDeclaration): boolean {
|
|
|
455
451
|
|
|
456
452
|
/** The name an expression spells a class by: an identifier, or the member a property access names. */
|
|
457
453
|
function spelledName(expression: Node): Identifier | undefined {
|
|
458
|
-
const node =
|
|
454
|
+
const node = parenthesesRemoved(expression);
|
|
459
455
|
if (isIdentifier(node)) {
|
|
460
456
|
return node;
|
|
461
457
|
}
|
|
@@ -724,8 +720,8 @@ export function projectParts<Brand>(
|
|
|
724
720
|
if ((isImportClause(node) || isNamespaceImport(node)) && node.name !== undefined) {
|
|
725
721
|
specifiers.add(position(node.name));
|
|
726
722
|
}
|
|
727
|
-
if (isExportAssignment(node) && isIdentifier(
|
|
728
|
-
specifiers.add(position(
|
|
723
|
+
if (isExportAssignment(node) && isIdentifier(parenthesesRemoved(node.expression))) {
|
|
724
|
+
specifiers.add(position(parenthesesRemoved(node.expression)));
|
|
729
725
|
}
|
|
730
726
|
if (isSwitchStatement(node)) {
|
|
731
727
|
const holder = holderOf(node);
|
|
@@ -1325,7 +1321,7 @@ function isPlainWrite(use: Identifier): boolean {
|
|
|
1325
1321
|
return (
|
|
1326
1322
|
isBinaryExpression(parent) &&
|
|
1327
1323
|
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
1328
|
-
|
|
1324
|
+
parenthesesRemoved(parent.left) === use
|
|
1329
1325
|
);
|
|
1330
1326
|
}
|
|
1331
1327
|
|
|
@@ -1382,14 +1378,14 @@ function deadStores(
|
|
|
1382
1378
|
if (!isExpressionStatement(statement)) {
|
|
1383
1379
|
return undefined;
|
|
1384
1380
|
}
|
|
1385
|
-
const expression =
|
|
1381
|
+
const expression = parenthesesRemoved(statement.expression);
|
|
1386
1382
|
if (
|
|
1387
1383
|
!isBinaryExpression(expression) ||
|
|
1388
1384
|
expression.operatorToken.kind !== SyntaxKind.EqualsToken
|
|
1389
1385
|
) {
|
|
1390
1386
|
return undefined;
|
|
1391
1387
|
}
|
|
1392
|
-
const target =
|
|
1388
|
+
const target = parenthesesRemoved(expression.left);
|
|
1393
1389
|
return isIdentifier(target) && uses.includes(target)
|
|
1394
1390
|
? { target, value: expression.right }
|
|
1395
1391
|
: undefined;
|
package/src/inventory.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
|
|
51
51
|
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
52
52
|
import type { Handle, ProjectView } from "./session.ts";
|
|
53
|
+
import { manifestAt } from "./fs/manifest.ts";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* The symbol inventory of one project: every declaration the target's own files
|
|
@@ -233,8 +234,6 @@ interface ExportScope {
|
|
|
233
234
|
readonly nameNode: Node;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
const MANIFEST = "package.json";
|
|
237
|
-
|
|
238
237
|
/** One manifest that names a package, and the directory below the root that holds it. */
|
|
239
238
|
interface Named {
|
|
240
239
|
readonly name: string;
|
|
@@ -255,24 +254,7 @@ export function packageScope(host: Host, root: string): (path: string) => Module
|
|
|
255
254
|
const named = new Map<string, Named | undefined>();
|
|
256
255
|
|
|
257
256
|
const manifestName = (dir: string): string | undefined => {
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
text = host.readFile(joinPath(root, dir, MANIFEST));
|
|
261
|
-
} catch {
|
|
262
|
-
return undefined;
|
|
263
|
-
}
|
|
264
|
-
let value: unknown;
|
|
265
|
-
try {
|
|
266
|
-
value = JSON.parse(text);
|
|
267
|
-
} catch {
|
|
268
|
-
// A manifest this program cannot read names no package, which is the same
|
|
269
|
-
// answer as an absent one: the file is then scoped by the target root.
|
|
270
|
-
return undefined;
|
|
271
|
-
}
|
|
272
|
-
if (typeof value !== "object" || value === null) {
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
const name = (value as Record<string, unknown>)["name"];
|
|
257
|
+
const name = manifestAt(host, joinPath(root, dir))?.["name"];
|
|
276
258
|
return typeof name === "string" && name !== "" ? name : undefined;
|
|
277
259
|
};
|
|
278
260
|
|
package/src/json-document.ts
CHANGED
|
@@ -1,122 +1,276 @@
|
|
|
1
|
+
import type { Position } from "./position.ts";
|
|
2
|
+
|
|
3
|
+
/** A place inside a document: each member name a string, each array index a number. */
|
|
4
|
+
export type Place = readonly (string | number)[];
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
|
-
* A
|
|
3
|
-
*
|
|
7
|
+
* A text that is not one strict JSON document: a syntax error, data after the value, a
|
|
8
|
+
* member one object writes twice, or nesting past {@link MAX_DEPTH}. `place` is the
|
|
9
|
+
* value the fault is in, followed by the member written twice.
|
|
4
10
|
*/
|
|
11
|
+
export class StrictJSONError extends Error {
|
|
12
|
+
readonly offset: number;
|
|
13
|
+
readonly place: Place;
|
|
5
14
|
|
|
6
|
-
|
|
15
|
+
constructor(reason: string, offset: number, place: Place) {
|
|
16
|
+
super(reason);
|
|
17
|
+
this.name = "StrictJSONError";
|
|
18
|
+
this.offset = offset;
|
|
19
|
+
this.place = place;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
7
22
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
readonly
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
/** One strict JSON document, read whole. */
|
|
24
|
+
interface StrictDocument {
|
|
25
|
+
readonly value: unknown;
|
|
26
|
+
/**
|
|
27
|
+
* The text of every number written with a fraction or an exponent, by its JSON
|
|
28
|
+
* Pointer: a value reads `100.0` as the integer 100, so only the text tells them apart.
|
|
29
|
+
*/
|
|
30
|
+
readonly fractions: ReadonlyMap<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* The offset of each value inside the top-level member a read names, by its index or
|
|
33
|
+
* its member name, in document order.
|
|
34
|
+
*/
|
|
35
|
+
readonly starts: ReadonlyMap<string | number, number>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How deep values may nest. A document this analyzer reads nests a few levels, and a
|
|
40
|
+
* bound turns a document built to exhaust the stack into a refusal.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_DEPTH = 1000;
|
|
43
|
+
|
|
44
|
+
const SPACE = /[ \t\n\r]*/uy;
|
|
45
|
+
const STRING =
|
|
46
|
+
/"(?:[ !#$%&'()*+,\-./0-9:;<=>?@A-Z[\]-\u{10FFFF}]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"/uy;
|
|
47
|
+
const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/uy;
|
|
48
|
+
const LITERALS: ReadonlyMap<string, unknown> = new Map<string, unknown>([
|
|
49
|
+
["true", true],
|
|
50
|
+
["false", false],
|
|
51
|
+
["null", null],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** One member name spelled as a JSON Pointer token. */
|
|
55
|
+
const escaped = (name: string): string => name.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
56
|
+
|
|
57
|
+
/** The member names and indexes a JSON Pointer holds, unescaped. */
|
|
58
|
+
export function pointerSegments(pointer: string): string[] {
|
|
59
|
+
return pointer === ""
|
|
60
|
+
? []
|
|
61
|
+
: pointer
|
|
62
|
+
.slice(1)
|
|
63
|
+
.split("/")
|
|
64
|
+
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The JSON Pointer of a place written as segments. */
|
|
68
|
+
export function pointerOf(segments: readonly string[]): string {
|
|
69
|
+
return segments.map((segment) => `/${escaped(segment)}`).join("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The place a JSON Pointer names inside `value`, followed by `member` where it is not
|
|
74
|
+
* empty: a pointer's text does not say whether a segment of digits is an index, so the
|
|
75
|
+
* value it points into does.
|
|
76
|
+
*/
|
|
77
|
+
export function placeIn(value: unknown, pointer: string, member = ""): (string | number)[] {
|
|
78
|
+
const place: (string | number)[] = [];
|
|
79
|
+
let held = value;
|
|
80
|
+
for (const segment of pointerSegments(pointer)) {
|
|
81
|
+
if (Array.isArray(held)) {
|
|
82
|
+
place.push(Number(segment));
|
|
83
|
+
held = held[Number(segment)];
|
|
84
|
+
} else {
|
|
85
|
+
place.push(segment);
|
|
86
|
+
held = isRecord(held) ? held[segment] : undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return member === "" ? place : [...place, member];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A place as a refusal names it: member names joined by full stops and an index in
|
|
94
|
+
* square brackets, as `ts.serializers[0].returns`.
|
|
95
|
+
*/
|
|
96
|
+
export function dotted(place: Place): string {
|
|
97
|
+
let path = "";
|
|
98
|
+
for (const segment of place) {
|
|
99
|
+
path +=
|
|
100
|
+
typeof segment === "number" ? `[${String(segment)}]` : path === "" ? segment : `.${segment}`;
|
|
101
|
+
}
|
|
102
|
+
return path;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class Reader {
|
|
15
106
|
private at = 0;
|
|
107
|
+
private readonly path: (string | number)[] = [];
|
|
108
|
+
readonly fractions = new Map<string, string>();
|
|
109
|
+
readonly starts = new Map<string | number, number>();
|
|
110
|
+
private readonly text: string;
|
|
111
|
+
private readonly records: string | undefined;
|
|
16
112
|
|
|
17
|
-
constructor(text: string,
|
|
113
|
+
constructor(text: string, records: string | undefined) {
|
|
18
114
|
this.text = text;
|
|
19
|
-
this.
|
|
115
|
+
this.records = records;
|
|
20
116
|
}
|
|
21
117
|
|
|
22
|
-
|
|
23
|
-
this.value(
|
|
118
|
+
read(): unknown {
|
|
119
|
+
const value = this.value(0);
|
|
120
|
+
this.match(SPACE);
|
|
121
|
+
if (this.at < this.text.length) {
|
|
122
|
+
this.fail("data follows the document's value");
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
24
125
|
}
|
|
25
126
|
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
127
|
+
private fail(reason: string, offset = this.at, member = ""): never {
|
|
128
|
+
throw new StrictJSONError(
|
|
129
|
+
reason,
|
|
130
|
+
offset,
|
|
131
|
+
member === "" ? [...this.path] : [...this.path, member],
|
|
132
|
+
);
|
|
30
133
|
}
|
|
31
134
|
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
135
|
+
private match(pattern: RegExp): string | undefined {
|
|
136
|
+
pattern.lastIndex = this.at;
|
|
137
|
+
const found = pattern.exec(this.text)?.[0];
|
|
138
|
+
this.at = found === undefined ? this.at : pattern.lastIndex;
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private expect(char: string, what: string): void {
|
|
143
|
+
this.match(SPACE);
|
|
144
|
+
if (this.text[this.at] !== char) {
|
|
145
|
+
this.fail(`${what} is expected`);
|
|
37
146
|
}
|
|
38
147
|
this.at += 1;
|
|
39
|
-
return JSON.parse(this.text.slice(start, this.at)) as string;
|
|
40
148
|
}
|
|
41
149
|
|
|
42
|
-
private value(
|
|
43
|
-
this.
|
|
44
|
-
if (
|
|
45
|
-
this.
|
|
150
|
+
private value(depth: number): unknown {
|
|
151
|
+
this.match(SPACE);
|
|
152
|
+
if (depth > MAX_DEPTH) {
|
|
153
|
+
this.fail(`values nest deeper than ${String(MAX_DEPTH)} levels`);
|
|
46
154
|
}
|
|
47
155
|
const char = this.text[this.at];
|
|
48
156
|
if (char === "{") {
|
|
49
|
-
this.object(
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
157
|
+
return this.object(depth);
|
|
158
|
+
}
|
|
159
|
+
if (char === "[") {
|
|
160
|
+
return this.array(depth);
|
|
161
|
+
}
|
|
162
|
+
const string = this.match(STRING);
|
|
163
|
+
if (string !== undefined) {
|
|
164
|
+
return JSON.parse(string) as string;
|
|
165
|
+
}
|
|
166
|
+
NUMBER.lastIndex = this.at;
|
|
167
|
+
const number = NUMBER.exec(this.text);
|
|
168
|
+
if (number !== null) {
|
|
169
|
+
this.at = NUMBER.lastIndex;
|
|
170
|
+
if (number[1] !== undefined || number[2] !== undefined) {
|
|
171
|
+
this.fractions.set(pointerOf(this.path.map(String)), number[0]);
|
|
57
172
|
}
|
|
173
|
+
return Number(number[0]);
|
|
58
174
|
}
|
|
175
|
+
for (const [literal, held] of LITERALS) {
|
|
176
|
+
if (this.text.startsWith(literal, this.at)) {
|
|
177
|
+
this.at += literal.length;
|
|
178
|
+
return held;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return this.fail(
|
|
182
|
+
char === undefined ? "the document ends where a value is expected" : "a value is expected",
|
|
183
|
+
);
|
|
59
184
|
}
|
|
60
185
|
|
|
61
|
-
private object(
|
|
62
|
-
const
|
|
186
|
+
private object(depth: number): Record<string, unknown> {
|
|
187
|
+
const held: Record<string, unknown> = {};
|
|
63
188
|
this.at += 1;
|
|
189
|
+
this.match(SPACE);
|
|
190
|
+
if (this.text[this.at] === "}") {
|
|
191
|
+
this.at += 1;
|
|
192
|
+
return held;
|
|
193
|
+
}
|
|
64
194
|
for (;;) {
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
195
|
+
this.match(SPACE);
|
|
196
|
+
const offset = this.at;
|
|
197
|
+
const spelled = this.match(STRING);
|
|
198
|
+
if (spelled === undefined) {
|
|
199
|
+
return this.fail("a member name is expected");
|
|
69
200
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.
|
|
201
|
+
const name = JSON.parse(spelled) as string;
|
|
202
|
+
if (Object.hasOwn(held, name)) {
|
|
203
|
+
this.fail(
|
|
204
|
+
`the member ${JSON.stringify(name)} is written twice, so neither value is chosen`,
|
|
205
|
+
offset,
|
|
206
|
+
name,
|
|
207
|
+
);
|
|
73
208
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
209
|
+
this.expect(":", "a colon after the member name");
|
|
210
|
+
if (depth === 1 && this.path[0] === this.records) {
|
|
211
|
+
this.match(SPACE);
|
|
212
|
+
this.starts.set(name, this.at);
|
|
78
213
|
}
|
|
79
|
-
|
|
80
|
-
|
|
214
|
+
this.path.push(name);
|
|
215
|
+
Object.defineProperty(held, name, {
|
|
216
|
+
value: this.value(depth + 1),
|
|
217
|
+
enumerable: true,
|
|
218
|
+
writable: true,
|
|
219
|
+
configurable: true,
|
|
220
|
+
});
|
|
221
|
+
this.path.pop();
|
|
222
|
+
this.match(SPACE);
|
|
223
|
+
const next = this.text[this.at];
|
|
81
224
|
this.at += 1;
|
|
82
|
-
|
|
225
|
+
if (next === "}") {
|
|
226
|
+
return held;
|
|
227
|
+
}
|
|
228
|
+
if (next !== ",") {
|
|
229
|
+
this.at -= 1;
|
|
230
|
+
this.fail("a comma or a closing brace is expected");
|
|
231
|
+
}
|
|
83
232
|
}
|
|
84
233
|
}
|
|
85
234
|
|
|
86
|
-
private
|
|
235
|
+
private array(depth: number): unknown[] {
|
|
236
|
+
const held: unknown[] = [];
|
|
237
|
+
const records = depth === 1 && this.path[0] === this.records;
|
|
87
238
|
this.at += 1;
|
|
239
|
+
this.match(SPACE);
|
|
240
|
+
if (this.text[this.at] === "]") {
|
|
241
|
+
this.at += 1;
|
|
242
|
+
return held;
|
|
243
|
+
}
|
|
88
244
|
for (;;) {
|
|
89
|
-
this.
|
|
90
|
-
if (
|
|
91
|
-
this.at
|
|
92
|
-
|
|
245
|
+
this.match(SPACE);
|
|
246
|
+
if (records) {
|
|
247
|
+
this.starts.set(held.length, this.at);
|
|
248
|
+
}
|
|
249
|
+
this.path.push(held.length);
|
|
250
|
+
held.push(this.value(depth + 1));
|
|
251
|
+
this.path.pop();
|
|
252
|
+
this.match(SPACE);
|
|
253
|
+
const next = this.text[this.at];
|
|
254
|
+
this.at += 1;
|
|
255
|
+
if (next === "]") {
|
|
256
|
+
return held;
|
|
93
257
|
}
|
|
94
|
-
if (
|
|
95
|
-
this.at
|
|
258
|
+
if (next !== ",") {
|
|
259
|
+
this.at -= 1;
|
|
260
|
+
this.fail("a comma or a closing bracket is expected");
|
|
96
261
|
}
|
|
97
|
-
this.value(element);
|
|
98
262
|
}
|
|
99
263
|
}
|
|
100
264
|
}
|
|
101
265
|
|
|
102
|
-
/** What a walk of one document found. */
|
|
103
|
-
interface DocumentWalk {
|
|
104
|
-
/** The offset of each element of the document's array member, in order: an object's `{`. */
|
|
105
|
-
readonly starts: readonly number[];
|
|
106
|
-
/** The first member one object writes twice, at any depth, as the offset of its name. */
|
|
107
|
-
readonly twice: { readonly name: string; readonly offset: number } | undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
266
|
/**
|
|
111
|
-
*
|
|
112
|
-
* records
|
|
113
|
-
* later value silently replacing the earlier one, so a document holding one is one a
|
|
114
|
-
* reader names rather than reads.
|
|
267
|
+
* Reads `text` as one strict JSON document, or throws a {@link StrictJSONError}.
|
|
268
|
+
* `records` names the top-level member whose values' offsets the read keeps.
|
|
115
269
|
*/
|
|
116
|
-
export function
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
return {
|
|
270
|
+
export function readStrict(text: string, records?: string): StrictDocument {
|
|
271
|
+
const reader = new Reader(text, records);
|
|
272
|
+
const value = reader.read();
|
|
273
|
+
return { value, fractions: reader.fractions, starts: reader.starts };
|
|
120
274
|
}
|
|
121
275
|
|
|
122
276
|
/**
|
|
@@ -128,3 +282,14 @@ export function positionAt(text: string, file: string, offset: number): Position
|
|
|
128
282
|
const lineStart = before.lastIndexOf("\n") + 1;
|
|
129
283
|
return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
|
|
130
284
|
}
|
|
285
|
+
|
|
286
|
+
/** A strict read's fault with the line and column it is at, as a refusal states it. */
|
|
287
|
+
export function locatedFault(text: string, error: StrictJSONError): string {
|
|
288
|
+
const { line, column } = positionAt(text, "", error.offset);
|
|
289
|
+
return `line ${String(line)}, column ${String(column)}: ${error.message}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Whether a decoded JSON value is an object: not `null` and not an array. */
|
|
293
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
294
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
295
|
+
}
|
package/src/keyof-comparisons.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
isCaseClause,
|
|
11
11
|
isElementAccessExpression,
|
|
12
12
|
isIdentifier,
|
|
13
|
-
isParenthesizedExpression,
|
|
14
13
|
isPropertyAccessExpression,
|
|
15
14
|
isSwitchStatement,
|
|
16
15
|
isTypeOperatorNode,
|
|
@@ -27,6 +26,7 @@ import { renderPosition } from "./position.ts";
|
|
|
27
26
|
import { UNANSWERED } from "./query.ts";
|
|
28
27
|
import type { Reference } from "./references.ts";
|
|
29
28
|
import type { ProjectView } from "./session.ts";
|
|
29
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
30
30
|
|
|
31
31
|
/** The operators that compare their operands for equality. */
|
|
32
32
|
const EQUALITIES: ReadonlySet<SyntaxKind> = new Set([
|
|
@@ -44,13 +44,9 @@ interface Comparison {
|
|
|
44
44
|
readonly other: Expression;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function unwrapped(node: Expression): Expression {
|
|
48
|
-
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
/** The name node an operand's symbol is read at: an identifier, or a property access's name. */
|
|
52
48
|
function nameOf(node: Expression): Node | undefined {
|
|
53
|
-
const at =
|
|
49
|
+
const at = parenthesesRemoved(node);
|
|
54
50
|
if (isIdentifier(at)) {
|
|
55
51
|
return at;
|
|
56
52
|
}
|
|
@@ -132,7 +128,7 @@ export function keyofComparisonReferences<Brand>(
|
|
|
132
128
|
}
|
|
133
129
|
const named = comparisons.flatMap((one) => {
|
|
134
130
|
const name = nameOf(one.keyed);
|
|
135
|
-
return name === undefined || isElementAccessExpression(
|
|
131
|
+
return name === undefined || isElementAccessExpression(parenthesesRemoved(one.other))
|
|
136
132
|
? []
|
|
137
133
|
: [{ ...one, name }];
|
|
138
134
|
});
|
package/src/kinds.ts
CHANGED
|
@@ -5,14 +5,6 @@ export const KINDS: ReadonlyMap<string, KindRow> = new Map(
|
|
|
5
5
|
KIND_ROWS.map((one): [string, KindRow] => [one.code, one]),
|
|
6
6
|
);
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* The code of every live issue kind, in ascending order. A severity key names one of
|
|
10
|
-
* them or a family prefix whose range holds one; a key naming a retired code, an
|
|
11
|
-
* unassigned code or a range holding no live kind is an unimplemented key, so every
|
|
12
|
-
* severity a configuration sets is one some analyzer reports under.
|
|
13
|
-
*/
|
|
14
|
-
export const LIVE_CODES: readonly string[] = KIND_ROWS.map((one) => one.code);
|
|
15
|
-
|
|
16
8
|
/** The issue-kind codes whose severity the Contract fixes, in ascending order. */
|
|
17
9
|
export const FIXED_SEVERITY_CODES: readonly string[] = KIND_ROWS.filter((one) => one.fixed).map(
|
|
18
10
|
(one) => one.code,
|
package/src/manifest.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Host } from "./host.ts";
|
|
9
|
-
import { isAbsolutePath,
|
|
9
|
+
import { isAbsolutePath, relativePath, resolvePath } from "./paths.ts";
|
|
10
|
+
import { isRecord } from "./json-document.ts";
|
|
11
|
+
import { MANIFEST, manifestAt } from "./fs/manifest.ts";
|
|
12
|
+
import { scriptTokens } from "./text/shell.ts";
|
|
10
13
|
|
|
11
14
|
/** The manifest's own name. */
|
|
12
|
-
const MANIFEST = "package.json";
|
|
13
15
|
|
|
14
16
|
/** The three members that name an entry point beside `exports` and `bin`. */
|
|
15
17
|
const LEGACY_MEMBERS = ["main", "module", "types"] as const;
|
|
@@ -44,10 +46,6 @@ export interface ManifestEntry {
|
|
|
44
46
|
readonly dir: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
49
|
/**
|
|
52
50
|
* Whether one member's value names a file of this package rather than another
|
|
53
51
|
* package or a URL. A target written with no leading dot is a path too, which is
|
|
@@ -60,24 +58,6 @@ function isTarget(value: string, requireDot: boolean): boolean {
|
|
|
60
58
|
return !requireDot || value.startsWith("./");
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
/** Where a script's command line splits into tokens: white space and the shell's operators. */
|
|
64
|
-
const SCRIPT_SEPARATORS = /\s+|&&|\|\||;|\|/u;
|
|
65
|
-
|
|
66
|
-
/** A token written inside one pair of quotes. */
|
|
67
|
-
const QUOTED = /^(["'])(.*)\1$/su;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Every token one script's command line holds, each with one pair of enclosing quotes
|
|
71
|
-
* removed. A token holding a wildcard is a pattern rather than a path, and names no
|
|
72
|
-
* entry point.
|
|
73
|
-
*/
|
|
74
|
-
export function scriptTokens(command: string): readonly string[] {
|
|
75
|
-
return command
|
|
76
|
-
.split(SCRIPT_SEPARATORS)
|
|
77
|
-
.map((token) => token.replace(QUOTED, "$2"))
|
|
78
|
-
.filter((token) => token !== "" && !token.includes("*"));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
61
|
/** One member's spelling below another: `exports` then `exports["."]`. */
|
|
82
62
|
function below(member: string, key: string): string {
|
|
83
63
|
return `${member}[${JSON.stringify(key)}]`;
|
|
@@ -111,27 +91,6 @@ function exportTargets(value: unknown, member: string, found: Omit<ManifestEntry
|
|
|
111
91
|
}
|
|
112
92
|
}
|
|
113
93
|
|
|
114
|
-
/** The manifest at the target root, decoded, or undefined where there is none to read. */
|
|
115
|
-
function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | undefined {
|
|
116
|
-
let text: string;
|
|
117
|
-
try {
|
|
118
|
-
text = host.readFile(joinPath(targetRoot, MANIFEST));
|
|
119
|
-
} catch {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
let value: unknown;
|
|
123
|
-
try {
|
|
124
|
-
value = JSON.parse(text);
|
|
125
|
-
} catch {
|
|
126
|
-
// A manifest this program cannot read names no entry point, which is the same
|
|
127
|
-
// answer as an absent one. What a manifest's contents say is not refused here:
|
|
128
|
-
// the package manager owns that refusal, and an analysis of a tree it rejects
|
|
129
|
-
// is not the run's subject.
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
return isRecord(value) ? value : undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
94
|
/** What one manifest declares about where its package is entered. */
|
|
136
95
|
export interface Manifest {
|
|
137
96
|
/**
|
|
@@ -158,7 +117,7 @@ export interface Manifest {
|
|
|
158
117
|
* it and there is one such configuration per project, while there is one manifest.
|
|
159
118
|
*/
|
|
160
119
|
export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
161
|
-
const manifest =
|
|
120
|
+
const manifest = manifestAt(host, targetRoot);
|
|
162
121
|
if (manifest === undefined) {
|
|
163
122
|
return { entries: [], declaresExports: false };
|
|
164
123
|
}
|