@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3
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/src/analysis.js +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- 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/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -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
|
@@ -139,8 +139,6 @@ export interface InventoryCost {
|
|
|
139
139
|
|
|
140
140
|
/** One project's declarations, in position order, and what reading them cost. */
|
|
141
141
|
export interface Inventory {
|
|
142
|
-
/** The compiler configuration the project was opened from. */
|
|
143
|
-
readonly configFile: string;
|
|
144
142
|
readonly symbols: readonly InventorySymbol[];
|
|
145
143
|
/**
|
|
146
144
|
* Each declaring node's {@link nodeKey} mapped to the identifier of the declaration
|
|
@@ -154,20 +152,6 @@ export interface Inventory {
|
|
|
154
152
|
*/
|
|
155
153
|
readonly declarations: ReadonlyMap<string, string>;
|
|
156
154
|
readonly cost: InventoryCost;
|
|
157
|
-
/**
|
|
158
|
-
* The module specifiers of the bare star re-exports the project's files carry, in
|
|
159
|
-
* the order they were read. A bare star re-export declares nothing here: the names
|
|
160
|
-
* it carries forward are the declarations of the module it names, so it is a
|
|
161
|
-
* reference to that module and not a declaration of this one.
|
|
162
|
-
*/
|
|
163
|
-
readonly starReExports: readonly string[];
|
|
164
|
-
/**
|
|
165
|
-
* The number of member declarations that were left out because the file declaring
|
|
166
|
-
* them is not one of the target's own files. Such a declaration belongs to another
|
|
167
|
-
* program, so the target's inventory does not name it, and the count is reported so
|
|
168
|
-
* that the omission is a number rather than a silence.
|
|
169
|
-
*/
|
|
170
|
-
readonly outsideOwnFiles: number;
|
|
171
155
|
/**
|
|
172
156
|
* The declarations whose module, namespace or container table the checker did not
|
|
173
157
|
* answer, in position order: whether each is exported, and which members a table
|
|
@@ -181,6 +165,12 @@ export interface Inventory {
|
|
|
181
165
|
readonly byName: ReadonlyMap<string, readonly string[]>;
|
|
182
166
|
/** Each `declare module` declaration whose name is a string literal, by id, to that name. */
|
|
183
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[]>;
|
|
184
174
|
}
|
|
185
175
|
|
|
186
176
|
/** A declaration under construction, before its reference is rendered. */
|
|
@@ -534,9 +524,7 @@ export function inventory<Brand>(
|
|
|
534
524
|
const byNode = new Map<string, Building>();
|
|
535
525
|
const exportScopes: ExportScope[] = [];
|
|
536
526
|
const containers: Container[] = [];
|
|
537
|
-
const starReExports: string[] = [];
|
|
538
527
|
const modules: (readonly [Building, string])[] = [];
|
|
539
|
-
let outsideOwnFiles = 0;
|
|
540
528
|
|
|
541
529
|
const keep = (record: Building): Building => {
|
|
542
530
|
building.push(record);
|
|
@@ -782,14 +770,10 @@ export function inventory<Brand>(
|
|
|
782
770
|
}
|
|
783
771
|
}
|
|
784
772
|
|
|
785
|
-
function walkExportDeclaration(
|
|
786
|
-
node: { readonly exportClause?: Node; readonly moduleSpecifier?: Node },
|
|
787
|
-
owner: Building,
|
|
788
|
-
): void {
|
|
773
|
+
function walkExportDeclaration(node: { readonly exportClause?: Node }, owner: Building): void {
|
|
789
774
|
const clause = node.exportClause;
|
|
775
|
+
// A bare star re-export declares nothing: the names it carries are the named module's.
|
|
790
776
|
if (clause === undefined) {
|
|
791
|
-
const specifier = node.moduleSpecifier;
|
|
792
|
-
starReExports.push(specifier === undefined ? "" : literalTextOf(specifier));
|
|
793
777
|
return;
|
|
794
778
|
}
|
|
795
779
|
if (isNamespaceExport(clause)) {
|
|
@@ -876,7 +860,7 @@ export function inventory<Brand>(
|
|
|
876
860
|
memberTables += 1;
|
|
877
861
|
const table = project.queries.membersOf(symbol);
|
|
878
862
|
if (isAnswered(table)) {
|
|
879
|
-
|
|
863
|
+
readTable(container, table, seen, false);
|
|
880
864
|
} else {
|
|
881
865
|
unansweredOwners.add(container.owner);
|
|
882
866
|
}
|
|
@@ -885,7 +869,7 @@ export function inventory<Brand>(
|
|
|
885
869
|
memberTables += 1;
|
|
886
870
|
const table = project.queries.exportsOf(symbol);
|
|
887
871
|
if (isAnswered(table)) {
|
|
888
|
-
|
|
872
|
+
readTable(container, table, seen, container.isClass);
|
|
889
873
|
} else {
|
|
890
874
|
unansweredOwners.add(container.owner);
|
|
891
875
|
}
|
|
@@ -905,8 +889,7 @@ export function inventory<Brand>(
|
|
|
905
889
|
table: ReadonlyMap<string, TSSymbol>,
|
|
906
890
|
seen: Map<string, Building>,
|
|
907
891
|
areStatic: boolean,
|
|
908
|
-
):
|
|
909
|
-
let outside = 0;
|
|
892
|
+
): void {
|
|
910
893
|
for (const [, member] of table) {
|
|
911
894
|
const declarations = member.declarations
|
|
912
895
|
.map((handle) => project.declarationAt(handle))
|
|
@@ -916,7 +899,6 @@ export function inventory<Brand>(
|
|
|
916
899
|
}
|
|
917
900
|
const own = declarations.filter((held) => ownFiles.has(held.node.getSourceFile().fileName));
|
|
918
901
|
if (own.length === 0) {
|
|
919
|
-
outside += declarations.length;
|
|
920
902
|
continue;
|
|
921
903
|
}
|
|
922
904
|
// A merged symbol's table holds the members every one of its declarations writes,
|
|
@@ -925,7 +907,6 @@ export function inventory<Brand>(
|
|
|
925
907
|
keepMember(container, held.node, seen, areStatic);
|
|
926
908
|
}
|
|
927
909
|
}
|
|
928
|
-
return outside;
|
|
929
910
|
}
|
|
930
911
|
|
|
931
912
|
/**
|
|
@@ -1011,25 +992,48 @@ export function inventory<Brand>(
|
|
|
1011
992
|
return undefined;
|
|
1012
993
|
}
|
|
1013
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
|
+
|
|
1014
1020
|
const byId = new Map(building.map((record) => [positionKey(record.position), record]));
|
|
1015
1021
|
const symbols = building
|
|
1016
1022
|
.map(render)
|
|
1017
1023
|
.sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
|
|
1018
1024
|
|
|
1019
1025
|
return {
|
|
1020
|
-
configFile: project.configFile,
|
|
1021
1026
|
symbols,
|
|
1022
1027
|
declarations: new Map(
|
|
1023
1028
|
[...byNode].map(([key, record]) => [key, positionKey(record.position)] as const),
|
|
1024
1029
|
),
|
|
1025
1030
|
cost: { batches: 1, exportTables, memberTables },
|
|
1026
|
-
starReExports,
|
|
1027
|
-
outsideOwnFiles,
|
|
1028
1031
|
unanswered: symbols
|
|
1029
1032
|
.filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
|
|
1030
1033
|
.map((symbol) => symbol.id),
|
|
1031
1034
|
byName: namesOf(building),
|
|
1032
1035
|
modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
|
|
1036
|
+
dependents,
|
|
1033
1037
|
};
|
|
1034
1038
|
}
|
|
1035
1039
|
|
package/src/json-document.ts
CHANGED
|
@@ -100,7 +100,7 @@ class Walk {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** What a walk of one document found. */
|
|
103
|
-
|
|
103
|
+
interface DocumentWalk {
|
|
104
104
|
/** The offset of the brace opening each object of the document's array member, in order. */
|
|
105
105
|
readonly opened: readonly number[];
|
|
106
106
|
/** The first member one object writes twice, at any depth, as the offset of its name. */
|
|
@@ -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/unstable/ast";
|
|
23
|
+
import { SymbolFlags, type Type } from "@typescript/native/unstable/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
|
+
}
|