@cplieger/deadset-ts 7.0.0-dev.2 → 7.0.0-dev.4
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/README.md +1 -1
- package/dist/src/alias-chain.js +2 -2
- package/dist/src/analysis.js +89 -44
- package/dist/src/augmentations.js +14 -12
- package/dist/src/calls.js +2 -2
- package/dist/src/class-members.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +6 -6
- package/dist/src/configured-declarations.js +2 -2
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +71 -14
- package/dist/src/convention-rows.js +6 -2
- package/dist/src/conventions.js +4 -2
- package/dist/src/dependencies.js +81 -27
- package/dist/src/deprecation.js +1 -1
- package/dist/src/destructuring.js +2 -2
- package/dist/src/discover.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/entry-points.js +2 -2
- package/dist/src/enum-group.js +2 -2
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/findings/completion.js +46 -15
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/self-check.js +17 -4
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/framework-lifecycle.js +1 -1
- package/dist/src/html-entries.js +1 -1
- package/dist/src/implementations.js +2 -2
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -125
- package/dist/src/injection-container.js +1 -1
- package/dist/src/interface-satisfaction.js +2 -2
- package/dist/src/intra-function-parts.js +26 -12
- package/dist/src/inventory.js +26 -1
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/member-names.js +1 -1
- package/dist/src/module-declarations.js +1 -1
- package/dist/src/namespace-objects.js +1 -1
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/overrides.js +2 -2
- package/dist/src/query.js +4 -1
- package/dist/src/references.js +13 -2
- package/dist/src/reflective-lookup.js +2 -2
- package/dist/src/report.js +1 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +1 -1
- package/dist/src/serialization-contract.js +45 -16
- package/dist/src/session.js +15 -8
- package/dist/src/signatures.js +1 -1
- package/dist/src/stores.js +1 -1
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +54 -24
- package/dist/src/symbol-keys.js +1 -1
- package/dist/src/type-errors.js +1 -1
- package/dist/src/type-query-alias.js +1 -1
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-flow.js +1 -1
- package/dist/src/value-reach.js +8 -8
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +2 -2
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +2 -2
- package/src/analysis.ts +135 -52
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +2 -2
- package/src/class-members.ts +2 -2
- package/src/component-layer.ts +1 -1
- package/src/components.ts +103 -21
- package/src/configuration-files.ts +6 -5
- package/src/configured-declarations.ts +2 -6
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +82 -18
- package/src/convention-rows.ts +6 -2
- package/src/conventions.ts +11 -2
- package/src/decorator.ts +1 -1
- package/src/dependencies.ts +108 -31
- package/src/deprecation.ts +1 -1
- package/src/destructuring.ts +2 -2
- package/src/discover.ts +2 -2
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +2 -2
- package/src/enum-group.ts +2 -2
- package/src/erased-stores.ts +413 -0
- package/src/file-facts.ts +3 -1
- package/src/findings/completion.ts +54 -20
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +1 -1
- package/src/findings/self-check.ts +18 -4
- package/src/findings-pass.ts +12 -5
- package/src/framework-lifecycle.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/implementations.ts +2 -2
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +304 -146
- package/src/injection-container.ts +1 -1
- package/src/interface-satisfaction.ts +2 -2
- package/src/intra-function-parts.ts +42 -17
- package/src/inventory.ts +34 -2
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +6 -21
- package/src/matrix.ts +34 -2
- package/src/member-names.ts +1 -1
- package/src/module-declarations.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/outside-tests.ts +1 -1
- package/src/overrides.ts +2 -2
- package/src/position.ts +1 -1
- package/src/query.ts +12 -2
- package/src/references.ts +25 -3
- package/src/reflective-lookup.ts +2 -2
- package/src/report.ts +3 -1
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +2 -2
- package/src/serialization-contract.ts +55 -16
- package/src/session.ts +39 -32
- package/src/signatures.ts +1 -1
- package/src/source-files.ts +1 -1
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +15 -5
- package/src/suppress.ts +63 -28
- package/src/symbol-keys.ts +2 -2
- package/src/type-errors.ts +2 -2
- package/src/type-query-alias.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-flow.ts +1 -1
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/workflow-steps.ts +1 -1
- package/src/workspace-programs.ts +4 -4
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
type Expression,
|
|
26
26
|
type Node,
|
|
27
27
|
type SourceFile,
|
|
28
|
-
} from "@typescript/native/
|
|
29
|
-
import { SymbolFlags, type Type } from "@typescript/native/
|
|
28
|
+
} from "@typescript/native/ast";
|
|
29
|
+
import { SymbolFlags, type Type } from "@typescript/native/sync";
|
|
30
30
|
import { aliasChains } from "./alias-chain.ts";
|
|
31
31
|
import { emptyImplementation } from "./implementations.ts";
|
|
32
32
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
type Node,
|
|
67
67
|
type SourceFile,
|
|
68
68
|
type Statement,
|
|
69
|
-
} from "@typescript/native/
|
|
69
|
+
} from "@typescript/native/ast";
|
|
70
70
|
import type { FindingPosition } from "./finding.ts";
|
|
71
71
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
72
72
|
import { positionKey, renderPosition } from "./position.ts";
|
|
@@ -93,8 +93,17 @@ export interface PartFact {
|
|
|
93
93
|
readonly used: boolean;
|
|
94
94
|
/** Where the part is a parameter's name, what a function used as a value decides it by. */
|
|
95
95
|
readonly parameter?: ParameterPlace;
|
|
96
|
+
/** Where the part is a result, whether its type has a `then` member, as a promise does. */
|
|
97
|
+
readonly thenable?: boolean;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/**
|
|
101
|
+
* What a call does with its value: uses it, discards it as a statement of its own or the
|
|
102
|
+
* operand of `void`, or awaits it as a statement, which sequences on a thenable value and
|
|
103
|
+
* discards any other.
|
|
104
|
+
*/
|
|
105
|
+
type CallUse = "used" | "discarded" | "awaited";
|
|
106
|
+
|
|
98
107
|
/** What decides a parameter of a function used as a value: a caller may pass it more arguments. */
|
|
99
108
|
interface ParameterPlace {
|
|
100
109
|
/** Whether a parameter after this one, which is no binding-pattern name, is read. */
|
|
@@ -116,8 +125,8 @@ interface SignatureFact {
|
|
|
116
125
|
export interface ProjectParts {
|
|
117
126
|
readonly parts: readonly PartFact[];
|
|
118
127
|
readonly signatures: readonly SignatureFact[];
|
|
119
|
-
/** Each name written as the callee of a call, by position, and
|
|
120
|
-
readonly calls: ReadonlyMap<string,
|
|
128
|
+
/** Each name written as the callee of a call, by position, and what the call does with its value. */
|
|
129
|
+
readonly calls: ReadonlyMap<string, CallUse>;
|
|
121
130
|
/** Each name an import or an export specifier writes, by position, which uses no value. */
|
|
122
131
|
readonly specifiers: ReadonlySet<string>;
|
|
123
132
|
}
|
|
@@ -165,16 +174,15 @@ function calleeName(call: CallExpression): Identifier | undefined {
|
|
|
165
174
|
return isPropertyAccessExpression(callee) && isIdentifier(callee.name) ? callee.name : undefined;
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
* statement, or the operand of `void`.
|
|
171
|
-
*/
|
|
172
|
-
function discarded(call: CallExpression): boolean {
|
|
177
|
+
/** What a call does with its value. */
|
|
178
|
+
function callUse(call: CallExpression): CallUse {
|
|
173
179
|
const parent = outerParent(call);
|
|
174
180
|
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
175
|
-
return
|
|
181
|
+
return "discarded";
|
|
176
182
|
}
|
|
177
|
-
return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent))
|
|
183
|
+
return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent))
|
|
184
|
+
? "awaited"
|
|
185
|
+
: "used";
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
/** Whether one statement leaves its statement list on every path: the rest of the list never runs. */
|
|
@@ -473,7 +481,7 @@ export function projectParts<Brand>(
|
|
|
473
481
|
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
474
482
|
const parts: PartFact[] = [];
|
|
475
483
|
const signatures: SignatureFact[] = [];
|
|
476
|
-
const calls = new Map<string,
|
|
484
|
+
const calls = new Map<string, CallUse>();
|
|
477
485
|
const specifiers = new Set<string>();
|
|
478
486
|
const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
|
|
479
487
|
const contextual: ContextualParameter[] = [];
|
|
@@ -550,7 +558,7 @@ export function projectParts<Brand>(
|
|
|
550
558
|
if (isCallExpression(node)) {
|
|
551
559
|
const name = calleeName(node);
|
|
552
560
|
if (name !== undefined) {
|
|
553
|
-
calls.set(position(name),
|
|
561
|
+
calls.set(position(name), callUse(node));
|
|
554
562
|
}
|
|
555
563
|
}
|
|
556
564
|
if (isImportSpecifier(node) || isExportSpecifier(node)) {
|
|
@@ -1004,10 +1012,12 @@ function resultParts<Brand>(
|
|
|
1004
1012
|
root: string,
|
|
1005
1013
|
sites: readonly { readonly file: SourceFile; readonly declared: Declared }[],
|
|
1006
1014
|
references: readonly Reference[],
|
|
1007
|
-
calls: ReadonlyMap<string,
|
|
1015
|
+
calls: ReadonlyMap<string, CallUse>,
|
|
1008
1016
|
): PartFact[] {
|
|
1009
1017
|
const discardedAt = new Set(
|
|
1010
|
-
references
|
|
1018
|
+
references
|
|
1019
|
+
.filter((one) => (calls.get(positionKey(one.position)) ?? "used") !== "used")
|
|
1020
|
+
.map((one) => one.to),
|
|
1011
1021
|
);
|
|
1012
1022
|
const found: PartFact[] = [];
|
|
1013
1023
|
for (const { file, declared } of sites) {
|
|
@@ -1030,6 +1040,8 @@ function resultParts<Brand>(
|
|
|
1030
1040
|
continue;
|
|
1031
1041
|
}
|
|
1032
1042
|
const at = signature.type ?? (node as { readonly name?: Node }).name ?? node;
|
|
1043
|
+
const valued = project.queries.nonNullable(result);
|
|
1044
|
+
const members = isAnswered(valued) ? project.queries.propertiesOf(valued) : UNANSWERED;
|
|
1033
1045
|
found.push({
|
|
1034
1046
|
code: UNUSED_RESULT,
|
|
1035
1047
|
declaration: id,
|
|
@@ -1038,6 +1050,7 @@ function resultParts<Brand>(
|
|
|
1038
1050
|
position: spanOf(file, root, at),
|
|
1039
1051
|
message: "result is discarded at every call site",
|
|
1040
1052
|
used: false,
|
|
1053
|
+
thenable: !isAnswered(members) || members.some((member) => member.name === "then"),
|
|
1041
1054
|
});
|
|
1042
1055
|
}
|
|
1043
1056
|
return found;
|
|
@@ -1077,7 +1090,7 @@ export function intraFunctionFacts(
|
|
|
1077
1090
|
const known = parts.get(key);
|
|
1078
1091
|
parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
|
|
1079
1092
|
}
|
|
1080
|
-
const calls = new Map<string,
|
|
1093
|
+
const calls = new Map<string, CallUse>();
|
|
1081
1094
|
const specifiers = new Set<string>();
|
|
1082
1095
|
for (const one of projects) {
|
|
1083
1096
|
for (const [at, value] of one.calls) {
|
|
@@ -1089,6 +1102,7 @@ export function intraFunctionFacts(
|
|
|
1089
1102
|
}
|
|
1090
1103
|
const valued = new Set<string>();
|
|
1091
1104
|
const called = new Map<string, boolean>();
|
|
1105
|
+
const awaited = new Set<string>();
|
|
1092
1106
|
for (const reference of references) {
|
|
1093
1107
|
if (reference.use === "decorator" || reference.use === "evaluation") {
|
|
1094
1108
|
continue;
|
|
@@ -1096,7 +1110,10 @@ export function intraFunctionFacts(
|
|
|
1096
1110
|
const at = positionKey(reference.position);
|
|
1097
1111
|
const call = reference.consumer === undefined ? calls.get(at) : undefined;
|
|
1098
1112
|
if (call !== undefined) {
|
|
1099
|
-
called.set(reference.to, (called.get(reference.to) ?? true) && call);
|
|
1113
|
+
called.set(reference.to, (called.get(reference.to) ?? true) && call !== "used");
|
|
1114
|
+
if (call === "awaited") {
|
|
1115
|
+
awaited.add(reference.to);
|
|
1116
|
+
}
|
|
1100
1117
|
} else if (reference.consumer !== undefined || !specifiers.has(at)) {
|
|
1101
1118
|
valued.add(reference.to);
|
|
1102
1119
|
}
|
|
@@ -1115,8 +1132,16 @@ export function intraFunctionFacts(
|
|
|
1115
1132
|
.map((one) => one.declaration)
|
|
1116
1133
|
.filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)),
|
|
1117
1134
|
);
|
|
1135
|
+
// Awaiting a thenable result sequences on it, so a call that awaits one uses it.
|
|
1136
|
+
const sequenced = new Set(
|
|
1137
|
+
[...parts.values()]
|
|
1138
|
+
.filter((part) => part.thenable === true && awaited.has(part.declaration))
|
|
1139
|
+
.map((part) => part.declaration),
|
|
1140
|
+
);
|
|
1118
1141
|
const discardedEverywhere = new Set(
|
|
1119
|
-
[...called]
|
|
1142
|
+
[...called]
|
|
1143
|
+
.filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
|
|
1144
|
+
.map(([id]) => id),
|
|
1120
1145
|
);
|
|
1121
1146
|
return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
|
|
1122
1147
|
}
|
package/src/inventory.ts
CHANGED
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
ModifierFlags,
|
|
29
29
|
type Node,
|
|
30
30
|
type SourceFile,
|
|
31
|
-
} from "@typescript/native/
|
|
32
|
-
import type { Symbol as TSSymbol } from "@typescript/native/
|
|
31
|
+
} from "@typescript/native/ast";
|
|
32
|
+
import type { Symbol as TSSymbol } from "@typescript/native/sync";
|
|
33
33
|
import type { Host } from "./host.ts";
|
|
34
34
|
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
35
35
|
import {
|
|
@@ -165,6 +165,12 @@ export interface Inventory {
|
|
|
165
165
|
readonly byName: ReadonlyMap<string, readonly string[]>;
|
|
166
166
|
/** Each `declare module` declaration whose name is a string literal, by id, to that name. */
|
|
167
167
|
readonly modules: ReadonlyMap<string, string>;
|
|
168
|
+
/**
|
|
169
|
+
* Each enum member's dependents, by id: the later members of its declaration with no
|
|
170
|
+
* initializer, up to the first one with an initializer, whose values its deletion moves.
|
|
171
|
+
* A member with none has no entry.
|
|
172
|
+
*/
|
|
173
|
+
readonly dependents: ReadonlyMap<string, readonly string[]>;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
/** A declaration under construction, before its reference is rendered. */
|
|
@@ -986,6 +992,31 @@ export function inventory<Brand>(
|
|
|
986
992
|
return undefined;
|
|
987
993
|
}
|
|
988
994
|
|
|
995
|
+
const dependents = new Map<string, readonly string[]>();
|
|
996
|
+
for (const container of containers.filter((one) => one.memberKind === "enum-member")) {
|
|
997
|
+
const ids = container.members.map((member) => {
|
|
998
|
+
const record = byNode.get(nodeKey(member.getSourceFile(), member));
|
|
999
|
+
return record === undefined ? undefined : positionKey(record.position);
|
|
1000
|
+
});
|
|
1001
|
+
container.members.forEach((_member, index) => {
|
|
1002
|
+
const after: string[] = [];
|
|
1003
|
+
for (let next = index + 1; next < container.members.length; next += 1) {
|
|
1004
|
+
const member = container.members[next];
|
|
1005
|
+
if (member === undefined || !isEnumMember(member) || member.initializer !== undefined) {
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
const id = ids[next];
|
|
1009
|
+
if (id !== undefined) {
|
|
1010
|
+
after.push(id);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const id = ids[index];
|
|
1014
|
+
if (id !== undefined && after.length > 0) {
|
|
1015
|
+
dependents.set(id, after);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
989
1020
|
const byId = new Map(building.map((record) => [positionKey(record.position), record]));
|
|
990
1021
|
const symbols = building
|
|
991
1022
|
.map(render)
|
|
@@ -1002,6 +1033,7 @@ export function inventory<Brand>(
|
|
|
1002
1033
|
.map((symbol) => symbol.id),
|
|
1003
1034
|
byName: namesOf(building),
|
|
1004
1035
|
modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
|
|
1036
|
+
dependents,
|
|
1005
1037
|
};
|
|
1006
1038
|
}
|
|
1007
1039
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a comparison with a key reads: where one operand of `===`, `!==`, `==` or
|
|
3
|
+
* `!=`, or a `switch` expression a `case` label is compared with, is declared with a type
|
|
4
|
+
* that holds `keyof X`, the literal the other side has names the member of `X` under that
|
|
5
|
+
* key, because deleting the member makes the comparison a type error.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isBinaryExpression,
|
|
10
|
+
isCaseClause,
|
|
11
|
+
isElementAccessExpression,
|
|
12
|
+
isIdentifier,
|
|
13
|
+
isParenthesizedExpression,
|
|
14
|
+
isPropertyAccessExpression,
|
|
15
|
+
isSwitchStatement,
|
|
16
|
+
isTypeOperatorNode,
|
|
17
|
+
SyntaxKind,
|
|
18
|
+
type Expression,
|
|
19
|
+
type Node,
|
|
20
|
+
type SourceFile,
|
|
21
|
+
type TypeNode,
|
|
22
|
+
} from "@typescript/native/ast";
|
|
23
|
+
import { SymbolFlags, type Type } from "@typescript/native/sync";
|
|
24
|
+
import type { AliasChains } from "./alias-chain.ts";
|
|
25
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
26
|
+
import { renderPosition } from "./position.ts";
|
|
27
|
+
import { UNANSWERED } from "./query.ts";
|
|
28
|
+
import type { Reference } from "./references.ts";
|
|
29
|
+
import type { ProjectView } from "./session.ts";
|
|
30
|
+
|
|
31
|
+
/** The operators that compare their operands for equality. */
|
|
32
|
+
const EQUALITIES: ReadonlySet<SyntaxKind> = new Set([
|
|
33
|
+
SyntaxKind.EqualsEqualsEqualsToken,
|
|
34
|
+
SyntaxKind.ExclamationEqualsEqualsToken,
|
|
35
|
+
SyntaxKind.EqualsEqualsToken,
|
|
36
|
+
SyntaxKind.ExclamationEqualsToken,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** One comparison of a keyed operand with another, and the declaration that writes it. */
|
|
40
|
+
interface Comparison {
|
|
41
|
+
readonly file: SourceFile;
|
|
42
|
+
readonly from: string;
|
|
43
|
+
readonly keyed: Expression;
|
|
44
|
+
readonly other: Expression;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unwrapped(node: Expression): Expression {
|
|
48
|
+
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The name node an operand's symbol is read at: an identifier, or a property access's name. */
|
|
52
|
+
function nameOf(node: Expression): Node | undefined {
|
|
53
|
+
const at = unwrapped(node);
|
|
54
|
+
if (isIdentifier(at)) {
|
|
55
|
+
return at;
|
|
56
|
+
}
|
|
57
|
+
return isPropertyAccessExpression(at) ? at.name : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The operand of each `keyof` one type node holds, through every type node it is built of. */
|
|
61
|
+
function keyofOperands(node: TypeNode): readonly TypeNode[] {
|
|
62
|
+
const found: TypeNode[] = [];
|
|
63
|
+
const visit = (at: Node): void => {
|
|
64
|
+
if (isTypeOperatorNode(at) && at.operator === SyntaxKind.KeyOfKeyword) {
|
|
65
|
+
found.push(at.type);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
at.forEachChild(visit);
|
|
69
|
+
};
|
|
70
|
+
visit(node);
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The names a literal type or a union of them spells, or none. */
|
|
75
|
+
function literalNames(parts: readonly Type[]): readonly string[] {
|
|
76
|
+
const names: string[] = [];
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
if (part.isStringLiteralType()) {
|
|
79
|
+
names.push(part.value);
|
|
80
|
+
} else if (part.isNumberLiteralType()) {
|
|
81
|
+
names.push(String(part.value));
|
|
82
|
+
} else {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return names;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The references the key comparisons of one project's own files make to the members their
|
|
91
|
+
* literal side names, each made by the declaration holding the comparison.
|
|
92
|
+
*/
|
|
93
|
+
export function keyofComparisonReferences<Brand>(
|
|
94
|
+
project: ProjectView<Brand>,
|
|
95
|
+
held: Inventory,
|
|
96
|
+
chains: AliasChains,
|
|
97
|
+
root: string,
|
|
98
|
+
testFiles: ReadonlySet<string>,
|
|
99
|
+
): readonly Reference[] {
|
|
100
|
+
const queries = project.queries;
|
|
101
|
+
const comparisons: Comparison[] = [];
|
|
102
|
+
for (const file of project.ownSourceFiles()) {
|
|
103
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
104
|
+
if (fileId === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const visit = (node: Node, from: string): void => {
|
|
108
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
109
|
+
if (isBinaryExpression(node) && EQUALITIES.has(node.operatorToken.kind)) {
|
|
110
|
+
comparisons.push({ file, from: holder, keyed: node.left, other: node.right });
|
|
111
|
+
comparisons.push({ file, from: holder, keyed: node.right, other: node.left });
|
|
112
|
+
}
|
|
113
|
+
if (isSwitchStatement(node)) {
|
|
114
|
+
for (const clause of node.caseBlock.clauses) {
|
|
115
|
+
if (isCaseClause(clause)) {
|
|
116
|
+
comparisons.push({
|
|
117
|
+
file,
|
|
118
|
+
from: holder,
|
|
119
|
+
keyed: node.expression,
|
|
120
|
+
other: clause.expression,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
node.forEachChild((child) => {
|
|
126
|
+
visit(child, holder);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
file.forEachChild((child) => {
|
|
130
|
+
visit(child, fileId);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const named = comparisons.flatMap((one) => {
|
|
134
|
+
const name = nameOf(one.keyed);
|
|
135
|
+
return name === undefined || isElementAccessExpression(unwrapped(one.other))
|
|
136
|
+
? []
|
|
137
|
+
: [{ ...one, name }];
|
|
138
|
+
});
|
|
139
|
+
if (named.length === 0) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The operand's own declaration states the key type, which the checker resolves to
|
|
144
|
+
// the literals it admits and so no longer names the type the key indexes.
|
|
145
|
+
const symbols = queries.symbolsAt(named.map((one) => one.name));
|
|
146
|
+
const keyed = named.flatMap((one, at) => {
|
|
147
|
+
const asked = symbols[at];
|
|
148
|
+
const symbol =
|
|
149
|
+
asked === UNANSWERED || asked === undefined || (asked.flags & SymbolFlags.Alias) === 0
|
|
150
|
+
? asked
|
|
151
|
+
: queries.aliased(asked);
|
|
152
|
+
if (symbol === UNANSWERED || symbol === undefined) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const operands = symbol.declarations.flatMap((handle) => {
|
|
156
|
+
const declared = (
|
|
157
|
+
project.declarationAt(handle)?.node as { readonly type?: TypeNode } | undefined
|
|
158
|
+
)?.type;
|
|
159
|
+
return declared === undefined ? [] : keyofOperands(declared);
|
|
160
|
+
});
|
|
161
|
+
return operands.length === 0 ? [] : [{ ...one, operands }];
|
|
162
|
+
});
|
|
163
|
+
if (keyed.length === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const nodes = keyed.flatMap((one) => [one.other, ...one.operands]);
|
|
167
|
+
const types = queries.typesAt(nodes);
|
|
168
|
+
const typeOf = new Map<Node, Type>();
|
|
169
|
+
nodes.forEach((node, at) => {
|
|
170
|
+
const type = types[at];
|
|
171
|
+
if (type !== UNANSWERED && type !== undefined) {
|
|
172
|
+
typeOf.set(node, type);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const partsOf = (type: Type): readonly Type[] => {
|
|
176
|
+
const parts = queries.constituents(type);
|
|
177
|
+
return parts === UNANSWERED ? [type] : parts;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const found: Reference[] = [];
|
|
181
|
+
for (const one of keyed) {
|
|
182
|
+
const literal = typeOf.get(one.other);
|
|
183
|
+
const names = literal === undefined ? [] : literalNames(partsOf(literal));
|
|
184
|
+
if (names.length === 0) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const position = renderPosition(one.file, root, one.other.getStart());
|
|
188
|
+
for (const operand of one.operands) {
|
|
189
|
+
const object = typeOf.get(operand);
|
|
190
|
+
for (const part of object === undefined ? [] : partsOf(object)) {
|
|
191
|
+
const properties = queries.propertiesOf(part);
|
|
192
|
+
for (const property of properties === UNANSWERED ? [] : properties) {
|
|
193
|
+
if (!names.includes(property.name)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
for (const to of chains.declarationsOf(property)) {
|
|
197
|
+
found.push({
|
|
198
|
+
from: one.from,
|
|
199
|
+
to,
|
|
200
|
+
position,
|
|
201
|
+
use: "read",
|
|
202
|
+
resolution: "keyed",
|
|
203
|
+
test: testFiles.has(position.path),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return found;
|
|
211
|
+
}
|
package/src/kinds.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface KindRow {
|
|
|
19
19
|
/** The highest confidence a finding of the kind carries. */
|
|
20
20
|
readonly maxClass: Confidence;
|
|
21
21
|
readonly fixability: Fixability;
|
|
22
|
+
/** Each fixability a finding of the kind may carry in place of `fixability`. */
|
|
23
|
+
readonly findingFixabilities?: readonly Fixability[];
|
|
22
24
|
/**
|
|
23
25
|
* Whether the Contract fixes the kind on at its default severity, so a severity key
|
|
24
26
|
* naming the code, or a family prefix whose range holds it, is an unimplemented key.
|
|
@@ -43,6 +45,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
43
45
|
defaultSeverity: "deny",
|
|
44
46
|
maxClass: "certain",
|
|
45
47
|
fixability: "deletable",
|
|
48
|
+
findingFixabilities: ["manual"],
|
|
46
49
|
fixed: false,
|
|
47
50
|
},
|
|
48
51
|
{
|
|
@@ -53,6 +56,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
53
56
|
defaultSeverity: "deny",
|
|
54
57
|
maxClass: "certain",
|
|
55
58
|
fixability: "deletable",
|
|
59
|
+
findingFixabilities: ["manual"],
|
|
56
60
|
fixed: false,
|
|
57
61
|
},
|
|
58
62
|
{
|
|
@@ -65,6 +69,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
65
69
|
defaultSeverity: "deny",
|
|
66
70
|
maxClass: "certain",
|
|
67
71
|
fixability: "deletable",
|
|
72
|
+
findingFixabilities: ["manual"],
|
|
68
73
|
fixed: false,
|
|
69
74
|
},
|
|
70
75
|
{
|
|
@@ -75,6 +80,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
75
80
|
defaultSeverity: "deny",
|
|
76
81
|
maxClass: "certain",
|
|
77
82
|
fixability: "deletable",
|
|
83
|
+
findingFixabilities: ["manual"],
|
|
78
84
|
fixed: false,
|
|
79
85
|
},
|
|
80
86
|
{
|
|
@@ -85,6 +91,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
85
91
|
defaultSeverity: "deny",
|
|
86
92
|
maxClass: "certain",
|
|
87
93
|
fixability: "deletable",
|
|
94
|
+
findingFixabilities: ["manual"],
|
|
88
95
|
fixed: false,
|
|
89
96
|
},
|
|
90
97
|
{
|
|
@@ -95,6 +102,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
95
102
|
defaultSeverity: "deny",
|
|
96
103
|
maxClass: "certain",
|
|
97
104
|
fixability: "deletable",
|
|
105
|
+
findingFixabilities: ["manual"],
|
|
98
106
|
fixed: false,
|
|
99
107
|
},
|
|
100
108
|
{
|
|
@@ -103,7 +111,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
103
111
|
languages: ["go", "ts"],
|
|
104
112
|
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
|
|
105
113
|
precondition:
|
|
106
|
-
"Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
|
|
114
|
+
"Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name. A package-level declaration whose name a string literal spells carries the class grammar/analysis.md gives it.",
|
|
107
115
|
defaultSeverity: "warn",
|
|
108
116
|
maxClass: "certain",
|
|
109
117
|
fixability: "narrowable",
|
|
@@ -125,10 +133,11 @@ const ROWS: readonly KindRow[] = [
|
|
|
125
133
|
code: "DS1103",
|
|
126
134
|
name: "unreachable-export",
|
|
127
135
|
languages: ["go", "ts"],
|
|
128
|
-
rule: "An unused exported symbol in a package or file no external code can import: an unused-exported candidate whose enclosing package or file is unimportable from outside, reported once under this code at the certain class, because unimportability is a property of the package graph rather than of the consumer set.",
|
|
136
|
+
rule: "An unused exported symbol in a package or file no external code can import: an unused-exported candidate whose enclosing package or file is unimportable from outside, reported once under this code at the certain class, because unimportability is a property of the package graph rather than of the consumer set, except where the confidence rule of grammar/analysis.md gives a declaration a string literal spells the class possible.",
|
|
129
137
|
defaultSeverity: "deny",
|
|
130
138
|
maxClass: "certain",
|
|
131
139
|
fixability: "deletable",
|
|
140
|
+
findingFixabilities: ["manual"],
|
|
132
141
|
fixed: false,
|
|
133
142
|
},
|
|
134
143
|
{
|
|
@@ -151,6 +160,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
151
160
|
defaultSeverity: "deny",
|
|
152
161
|
maxClass: "certain",
|
|
153
162
|
fixability: "deletable",
|
|
163
|
+
findingFixabilities: ["manual"],
|
|
154
164
|
fixed: false,
|
|
155
165
|
},
|
|
156
166
|
{
|
|
@@ -170,6 +180,8 @@ const ROWS: readonly KindRow[] = [
|
|
|
170
180
|
name: "unused-satisfaction-assertion",
|
|
171
181
|
languages: ["go"],
|
|
172
182
|
rule: "A compile-time satisfaction assertion whose interface no symbol uses as a type.",
|
|
183
|
+
precondition:
|
|
184
|
+
"In a library, an assertion whose interface is published API, an exported interface of a package outside programs can import, has the reachability class possible, whatever consumers the run declares and loads. The assertion checks a promise the published API makes, that the type satisfies the published interface, and that promise is made to every program that imports the package, not only to the consumers the run declares. A loaded consumer that names the interface as a type uses it, and the assertion is not reported.",
|
|
173
185
|
defaultSeverity: "deny",
|
|
174
186
|
maxClass: "certain",
|
|
175
187
|
fixability: "deletable",
|
|
@@ -179,7 +191,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
179
191
|
code: "DS1301",
|
|
180
192
|
name: "write-only-symbol",
|
|
181
193
|
languages: ["go", "ts"],
|
|
182
|
-
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
|
|
194
|
+
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
|
|
183
195
|
defaultSeverity: "deny",
|
|
184
196
|
maxClass: "certain",
|
|
185
197
|
fixability: "deletable",
|
|
@@ -193,6 +205,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
193
205
|
defaultSeverity: "deny",
|
|
194
206
|
maxClass: "certain",
|
|
195
207
|
fixability: "deletable",
|
|
208
|
+
findingFixabilities: ["manual"],
|
|
196
209
|
fixed: false,
|
|
197
210
|
},
|
|
198
211
|
{
|
|
@@ -211,7 +224,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
211
224
|
code: "DS1501",
|
|
212
225
|
name: "file-never-built",
|
|
213
226
|
languages: ["go", "ts"],
|
|
214
|
-
rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint
|
|
227
|
+
rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint holds only when the ignore tag is set is never reported, because grammar/analysis.md keeps such a file outside the analysis wherever no configuration builds it: the tag is the toolchain's convention for a file kept out of ordinary package builds unless a configuration explicitly satisfies the tag, and built by hand, the toolchain applying no build constraint to a file named on its own command line. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
|
|
215
228
|
precondition:
|
|
216
229
|
'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled. Such a file is recorded as excluded by cgo rather than as never built.',
|
|
217
230
|
defaultSeverity: "deny",
|
|
@@ -235,7 +248,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
235
248
|
languages: ["go", "ts"],
|
|
236
249
|
rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
|
|
237
250
|
precondition:
|
|
238
|
-
"On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and a call of `import.meta.resolve` or of `require.resolve` in them, whatever `require` names, whose first argument is a string literal or a template literal with no substitution needs each dependency an import of that specifier needs.
|
|
251
|
+
"On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and a call of `import.meta.resolve` or of `require.resolve` in them, whatever `require` names, whose first argument is a string literal or a template literal with no substitution needs each dependency an import of that specifier needs. Four more dependencies are needed: one whose command a script entry or a workflow step runs, one that a string of a configuration file names, and one that a string names under a manifest key or a package key of an applied convention row, each as grammar/analysis.md states, and one that a needed dependency names as a peer, as stated below. A command no script entry and no workflow step runs needs nothing. A declared dependency that the installed manifest of a needed declared dependency names in its peerDependencies is needed, and so on until no further dependency joins. A peer dependency is a package the needed dependency expects beside it at a version its manifest states, whether or not it imports the package, and the project's declaration is what keeps that package present at a version the project chooses, so removing the declaration can break the needed dependency where nothing the analysis reads imports the peer. A peer that peerDependenciesMeta marks optional is needed in the same way, because the needed dependency's manifest still names it as a package it works with, and a package manager may leave an optional peer out where the project does not declare it, so the project's declaration is what keeps it present for the needed dependency. A declared dependency that only dependencies nothing needs name as a peer is needed by nothing. A dependency of a manifest whose directory holds source only in configurations the run lists in configurations_not_built is reported at the reachability class possible, because the source that may import it was not analyzed. An installed manifest is the package's manifest in the nearest node_modules directory at or above the directory of the manifest that declares the dependency, and a dependency with none is decided by the import closure alone. On the TypeScript side, the dependencies judged are those of every manifest grammar/analysis.md says the analysis reads, a nested package's among them, each read against its own directory: a dependency is needed by a use written in a file at or below the directory of the manifest that declares it, by a use no one file holds, and as a peer of a needed dependency of the same manifest or of a manifest below it. A dependency a nested package's manifest declares whose installed manifest the analysis cannot find is not judged, because no run requires a nested package to be installed and its commands and peers cannot be read, while a workspace member's and the target's own are judged as above.",
|
|
239
252
|
defaultSeverity: "deny",
|
|
240
253
|
maxClass: "certain",
|
|
241
254
|
fixability: "manual",
|
|
@@ -277,7 +290,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
277
290
|
code: "DS1703",
|
|
278
291
|
name: "stale-suppression",
|
|
279
292
|
languages: ["go", "ts"],
|
|
280
|
-
rule: "A suppression that
|
|
293
|
+
rule: "A suppression record that is stale, as grammar/suppression.md defines it, at either mechanism and in the baseline alike. The run exits with the findings code when at least one is reported. The kind is fixed on at deny: no flag, no severity setting and no per-mechanism exception reduces it below a finding, and a configuration naming this code under a severity key is an unimplemented key.",
|
|
281
294
|
defaultSeverity: "deny",
|
|
282
295
|
maxClass: "certain",
|
|
283
296
|
fixability: "none",
|
|
@@ -345,7 +358,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
345
358
|
languages: ["go", "ts"],
|
|
346
359
|
rule: "A result value that no call site of the function uses, on a function whose signature is free to change.",
|
|
347
360
|
precondition:
|
|
348
|
-
"The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible.",
|
|
361
|
+
"The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
|
|
349
362
|
defaultSeverity: "warn",
|
|
350
363
|
maxClass: "certain",
|
|
351
364
|
fixability: "manual",
|
package/src/manifest.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Host } from "./host.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
10
10
|
|
|
11
11
|
/** The manifest's own name. */
|
|
12
12
|
const MANIFEST = "package.json";
|
|
@@ -212,31 +212,16 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
* The entry points
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* otherwise. Each entry is spelled under its manifest's path below the target root, and
|
|
219
|
-
* is published by its own manifest's rule, as the target's own entries are.
|
|
215
|
+
* The entry points the manifests of the nested packages in `dirs` name. Each entry is
|
|
216
|
+
* spelled under its manifest's path below the target root, and is published by its own
|
|
217
|
+
* manifest's rule, as the target's own entries are.
|
|
220
218
|
*/
|
|
221
219
|
export function packageEntries(
|
|
222
220
|
host: Host,
|
|
223
221
|
targetRoot: string,
|
|
224
|
-
|
|
225
|
-
known: ReadonlySet<string>,
|
|
222
|
+
dirs: readonly string[],
|
|
226
223
|
): readonly ManifestEntry[] {
|
|
227
|
-
|
|
228
|
-
for (const configFile of configFiles) {
|
|
229
|
-
for (
|
|
230
|
-
let dir = dirnamePath(resolvePath(targetRoot, configFile));
|
|
231
|
-
dir !== targetRoot && relativePath(targetRoot, dir) !== undefined;
|
|
232
|
-
dir = dirnamePath(dir)
|
|
233
|
-
) {
|
|
234
|
-
if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
|
|
235
|
-
dirs.add(dir);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return [...dirs].sort().flatMap((dir) => {
|
|
224
|
+
return dirs.flatMap((dir) => {
|
|
240
225
|
const manifest = readManifest(host, dir);
|
|
241
226
|
const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
|
|
242
227
|
return manifest.entries.map((entry) => ({
|