@qualety/typescript 0.1.0
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/LICENSE +21 -0
- package/dist/exhaustive-switch.d.ts +8 -0
- package/dist/exhaustive-switch.d.ts.map +1 -0
- package/dist/exhaustive-switch.js +198 -0
- package/dist/exhaustive-switch.js.map +1 -0
- package/dist/explicit-public-return-types.d.ts +8 -0
- package/dist/explicit-public-return-types.d.ts.map +1 -0
- package/dist/explicit-public-return-types.js +123 -0
- package/dist/explicit-public-return-types.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/narrowing.d.ts +36 -0
- package/dist/narrowing.d.ts.map +1 -0
- package/dist/narrowing.js +776 -0
- package/dist/narrowing.js.map +1 -0
- package/dist/no-constant-condition.d.ts +8 -0
- package/dist/no-constant-condition.d.ts.map +1 -0
- package/dist/no-constant-condition.js +59 -0
- package/dist/no-constant-condition.js.map +1 -0
- package/dist/no-empty-catch.d.ts +7 -0
- package/dist/no-empty-catch.d.ts.map +1 -0
- package/dist/no-empty-catch.js +23 -0
- package/dist/no-empty-catch.js.map +1 -0
- package/dist/no-export-star.d.ts +7 -0
- package/dist/no-export-star.d.ts.map +1 -0
- package/dist/no-export-star.js +30 -0
- package/dist/no-export-star.js.map +1 -0
- package/dist/no-floating-promises.d.ts +14 -0
- package/dist/no-floating-promises.d.ts.map +1 -0
- package/dist/no-floating-promises.js +129 -0
- package/dist/no-floating-promises.js.map +1 -0
- package/dist/no-misused-promises.d.ts +8 -0
- package/dist/no-misused-promises.d.ts.map +1 -0
- package/dist/no-misused-promises.js +188 -0
- package/dist/no-misused-promises.js.map +1 -0
- package/dist/no-non-null-assertion.d.ts +7 -0
- package/dist/no-non-null-assertion.d.ts.map +1 -0
- package/dist/no-non-null-assertion.js +24 -0
- package/dist/no-non-null-assertion.js.map +1 -0
- package/dist/no-public-any.d.ts +8 -0
- package/dist/no-public-any.d.ts.map +1 -0
- package/dist/no-public-any.js +92 -0
- package/dist/no-public-any.js.map +1 -0
- package/dist/no-unnecessary-abstraction.d.ts +8 -0
- package/dist/no-unnecessary-abstraction.d.ts.map +1 -0
- package/dist/no-unnecessary-abstraction.js +452 -0
- package/dist/no-unnecessary-abstraction.js.map +1 -0
- package/dist/no-unsafe-assertion.d.ts +7 -0
- package/dist/no-unsafe-assertion.d.ts.map +1 -0
- package/dist/no-unsafe-assertion.js +38 -0
- package/dist/no-unsafe-assertion.js.map +1 -0
- package/dist/parse-flow.d.ts +16 -0
- package/dist/parse-flow.d.ts.map +1 -0
- package/dist/parse-flow.js +136 -0
- package/dist/parse-flow.js.map +1 -0
- package/dist/public-exports-tested.d.ts +8 -0
- package/dist/public-exports-tested.d.ts.map +1 -0
- package/dist/public-exports-tested.js +170 -0
- package/dist/public-exports-tested.js.map +1 -0
- package/dist/ts-source.d.ts +5 -0
- package/dist/ts-source.d.ts.map +1 -0
- package/dist/ts-source.js +28 -0
- package/dist/ts-source.js.map +1 -0
- package/dist/type-narrowing-checks.d.ts +8 -0
- package/dist/type-narrowing-checks.d.ts.map +1 -0
- package/dist/type-narrowing-checks.js +99 -0
- package/dist/type-narrowing-checks.js.map +1 -0
- package/dist/zod-boundary.d.ts +8 -0
- package/dist/zod-boundary.d.ts.map +1 -0
- package/dist/zod-boundary.js +110 -0
- package/dist/zod-boundary.js.map +1 -0
- package/package.json +44 -0
- package/tsconfig/recommended.json +11 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { Node, SourceFile, SyntaxKind, } from "ts-morph";
|
|
2
|
+
import { aliasesOf, firstArgIdentifier, isFunctionLike, isSchemaParseCall, typeofTarget, } from "./parse-flow.js";
|
|
3
|
+
const TIGHTEN_OR_DROP = "Tighten the callee parameter type to match what callers already pass, or remove this internal check.";
|
|
4
|
+
const DROP_INTERNAL = "Remove this check or the dead branch; the parameter type already implies it.";
|
|
5
|
+
const DROP_RESTATED = "Remove the restated guard and use the already-narrowed binding.";
|
|
6
|
+
const DROP_PARSE = "Remove the second parse or restated hand-guard; use the parsed .data / narrowed binding.";
|
|
7
|
+
const DROP_LITERAL = "Remove this branch; the condition is constant.";
|
|
8
|
+
const DROP_CALLER = "Remove this guard if it exists only to satisfy the call.";
|
|
9
|
+
export function unwrapParens(node) {
|
|
10
|
+
let current = node;
|
|
11
|
+
while (Node.isParenthesizedExpression(current)) {
|
|
12
|
+
current = current.getExpression();
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
export function splitNegation(node) {
|
|
17
|
+
let current = unwrapParens(node);
|
|
18
|
+
let negated = false;
|
|
19
|
+
while (Node.isPrefixUnaryExpression(current) &&
|
|
20
|
+
current.getOperatorToken() === SyntaxKind.ExclamationToken) {
|
|
21
|
+
negated = !negated;
|
|
22
|
+
current = unwrapParens(current.getOperand());
|
|
23
|
+
}
|
|
24
|
+
return { inner: current, negated };
|
|
25
|
+
}
|
|
26
|
+
export function bindFunctionScan(scan) {
|
|
27
|
+
return (context) => {
|
|
28
|
+
const sources = context.getArtifact("typescript").sources;
|
|
29
|
+
for (const [abs, unit] of sources) {
|
|
30
|
+
if (!(unit instanceof SourceFile)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const reported = new Set();
|
|
34
|
+
unit.forEachDescendant((node) => {
|
|
35
|
+
if (isFunctionLike(node)) {
|
|
36
|
+
scan(node, abs, unit, context, reported);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function classifyCondition(fn, cond) {
|
|
43
|
+
const { inner } = splitNegation(unwrapParens(cond));
|
|
44
|
+
return classifyExpr(fn, inner);
|
|
45
|
+
}
|
|
46
|
+
export function conditionLeaves(fn, cond) {
|
|
47
|
+
const found = [];
|
|
48
|
+
const queue = [cond];
|
|
49
|
+
while (queue.length > 0) {
|
|
50
|
+
const expr = unwrapParens(queue.pop() ?? cond);
|
|
51
|
+
if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "&&") {
|
|
52
|
+
queue.push(expr.getLeft(), expr.getRight());
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const cand = classifyCondition(fn, expr);
|
|
56
|
+
if (cand !== undefined) {
|
|
57
|
+
found.push(cand);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
function classifyExpr(fn, node) {
|
|
63
|
+
const expr = unwrapParens(node);
|
|
64
|
+
return classifyIdent(fn, expr) ?? classifyCall(expr) ?? classifyBinary(expr);
|
|
65
|
+
}
|
|
66
|
+
function classifyIdent(fn, expr) {
|
|
67
|
+
if (!Node.isIdentifier(expr)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
let init;
|
|
71
|
+
fn.forEachDescendant((node) => {
|
|
72
|
+
if (Node.isVariableDeclaration(node) && node.getName() === expr.getText()) {
|
|
73
|
+
init = node.getInitializer();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (init !== undefined) {
|
|
77
|
+
const fromInit = classifyExpr(fn, init);
|
|
78
|
+
if (fromInit !== undefined) {
|
|
79
|
+
return fromInit;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const type = expr.getType();
|
|
83
|
+
if (!(type.isBoolean() && !type.isBooleanLiteral() && !type.isUnion()) &&
|
|
84
|
+
type.isUnion() &&
|
|
85
|
+
hasNullish(type)) {
|
|
86
|
+
return { node: expr, subject: expr.getText(), kind: "truthiness" };
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
function classifyCall(expr) {
|
|
91
|
+
if (!Node.isCallExpression(expr)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const arg = expr.getArguments()[0];
|
|
95
|
+
if (arg === undefined || !Node.isIdentifier(arg)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const subject = arg.getText();
|
|
99
|
+
const callee = expr.getExpression();
|
|
100
|
+
if (isSchemaParseCall(expr)) {
|
|
101
|
+
return { node: expr, subject, kind: "schema-parse" };
|
|
102
|
+
}
|
|
103
|
+
const guardName = Node.isIdentifier(callee)
|
|
104
|
+
? callee.getText()
|
|
105
|
+
: Node.isPropertyAccessExpression(callee)
|
|
106
|
+
? callee.getName()
|
|
107
|
+
: undefined;
|
|
108
|
+
if (hasTypePredicate(expr) || (guardName !== undefined && /^(is|assert)[A-Z]/.test(guardName))) {
|
|
109
|
+
return { node: expr, subject, kind: "predicate" };
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function classifyNullishBinary(node, op, left, right) {
|
|
114
|
+
if ((op !== "===" && op !== "==" && op !== "!==" && op !== "!=") ||
|
|
115
|
+
!(isNullishLiteral(left) || isNullishLiteral(right))) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (Node.isIdentifier(left) && isNullishLiteral(right)) {
|
|
119
|
+
return { node, subject: left.getText(), kind: "nullish" };
|
|
120
|
+
}
|
|
121
|
+
if (Node.isIdentifier(right) && isNullishLiteral(left)) {
|
|
122
|
+
return { node, subject: right.getText(), kind: "nullish" };
|
|
123
|
+
}
|
|
124
|
+
const objectName = objectVsNullish(left, right);
|
|
125
|
+
if (objectName !== undefined) {
|
|
126
|
+
return { node, subject: objectName, kind: "nullish" };
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
function classifyBinary(node) {
|
|
131
|
+
if (!Node.isBinaryExpression(node)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const op = node.getOperatorToken().getText();
|
|
135
|
+
const left = unwrapParens(node.getLeft());
|
|
136
|
+
const right = unwrapParens(node.getRight());
|
|
137
|
+
if (op === "instanceof" && Node.isIdentifier(left)) {
|
|
138
|
+
return { node, subject: left.getText(), kind: "instanceof" };
|
|
139
|
+
}
|
|
140
|
+
const typeofId = typeofTarget(left) ?? typeofTarget(right);
|
|
141
|
+
const lit = stringLit(left) ?? stringLit(right);
|
|
142
|
+
if (typeofId !== undefined &&
|
|
143
|
+
lit !== undefined &&
|
|
144
|
+
(op === "===" || op === "==" || op === "!==" || op === "!=")) {
|
|
145
|
+
return { node, subject: typeofId, kind: "typeof" };
|
|
146
|
+
}
|
|
147
|
+
const nullish = classifyNullishBinary(node, op, left, right);
|
|
148
|
+
if (nullish !== undefined) {
|
|
149
|
+
return nullish;
|
|
150
|
+
}
|
|
151
|
+
const nonEmpty = nonEmptySubject(node);
|
|
152
|
+
if (nonEmpty !== undefined) {
|
|
153
|
+
return { node, subject: nonEmpty, kind: "nonempty" };
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
export function diagnoseConstant(fn, cond, sourceFile) {
|
|
158
|
+
const literal = literalConstant(cond);
|
|
159
|
+
if (literal !== undefined) {
|
|
160
|
+
return literal;
|
|
161
|
+
}
|
|
162
|
+
const cand = classifyCondition(fn, cond);
|
|
163
|
+
if (cand === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const { negated } = splitNegation(unwrapParens(cond));
|
|
167
|
+
return (priorParseHit(fn, cand, cond, negated) ??
|
|
168
|
+
paramTypeHit(fn, cand, cond, negated) ??
|
|
169
|
+
priorGuardHit(fn, cand, cond, negated) ??
|
|
170
|
+
diagnoseAllCallers(fn, cand, cond, negated, sourceFile));
|
|
171
|
+
}
|
|
172
|
+
function priorParseHit(fn, cand, cond, negated) {
|
|
173
|
+
if (!hasPriorParse(fn, cand.subject, cond.getStart()) ||
|
|
174
|
+
(cand.kind !== "predicate" &&
|
|
175
|
+
cand.kind !== "typeof" &&
|
|
176
|
+
cand.kind !== "nullish" &&
|
|
177
|
+
cand.kind !== "schema-parse")) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
polarity: negated ? "false" : "true",
|
|
182
|
+
reason: "prior-parse",
|
|
183
|
+
suggestion: DROP_PARSE,
|
|
184
|
+
reportAt: cond,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function paramTypeHit(fn, cand, cond, negated) {
|
|
188
|
+
const param = fn.getParameters().find((item) => item.getName() === cand.subject);
|
|
189
|
+
if (param === undefined) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const implied = declaredTypeImplies(param, cand);
|
|
193
|
+
if (implied === undefined) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
polarity: (negated ? !implied : implied) ? "true" : "false",
|
|
198
|
+
reason: "param-type",
|
|
199
|
+
suggestion: DROP_INTERNAL,
|
|
200
|
+
reportAt: cond,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function priorGuardHit(fn, cand, cond, negated) {
|
|
204
|
+
if (!priorFactImplies(fn, cond, cand)) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
polarity: negated ? "false" : "true",
|
|
209
|
+
reason: "prior-guard",
|
|
210
|
+
suggestion: DROP_RESTATED,
|
|
211
|
+
reportAt: cond,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export function mixedHitsForCondition(fn, cond, sourceFile) {
|
|
215
|
+
const cand = classifyCondition(fn, cond);
|
|
216
|
+
if (cand === undefined) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const paramIndex = fn.getParameters().findIndex((item) => item.getName() === cand.subject);
|
|
220
|
+
if (paramIndex < 0) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
const calls = sameFileCalls(sourceFile, fn);
|
|
224
|
+
if (calls.length < 2) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const implying = calls.filter((call) => argImpliesGuard(call, paramIndex, cand));
|
|
228
|
+
if (implying.length === 0 || implying.length === calls.length) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return implying.flatMap((call) => mixedHitForCall(call, paramIndex, cand));
|
|
232
|
+
}
|
|
233
|
+
function mixedHitForCall(call, paramIndex, cand) {
|
|
234
|
+
const caller = enclosingFunction(call);
|
|
235
|
+
const arg = call.getArguments()[paramIndex];
|
|
236
|
+
if (caller === undefined || arg === undefined || !Node.isIdentifier(arg)) {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
const guard = wrappingGuard(call, caller, arg.getText(), cand);
|
|
240
|
+
if (guard === undefined || bindingUsedElsewhere(caller, arg.getText(), [call, guard])) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
polarity: "true",
|
|
246
|
+
reason: "call-site",
|
|
247
|
+
suggestion: DROP_CALLER,
|
|
248
|
+
reportAt: guard,
|
|
249
|
+
message: "This check is redundant given the subsequent call.",
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
export function secondParseNodes(fn) {
|
|
254
|
+
const seen = new Set();
|
|
255
|
+
const extras = [];
|
|
256
|
+
for (const node of ownNodes(fn)) {
|
|
257
|
+
if (!isSchemaParseCall(node)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const name = firstArgIdentifier(node);
|
|
261
|
+
if (name === undefined) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const names = aliasesOf(fn, name);
|
|
265
|
+
const key = [...names].sort().join(",");
|
|
266
|
+
if (seen.has(key) || [...names].some((alias) => seen.has(alias))) {
|
|
267
|
+
extras.push(node);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
for (const alias of names) {
|
|
271
|
+
seen.add(alias);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return extras;
|
|
275
|
+
}
|
|
276
|
+
export function hasPriorParse(fn, subject, before) {
|
|
277
|
+
const names = aliasesOf(fn, subject);
|
|
278
|
+
for (const node of ownNodes(fn)) {
|
|
279
|
+
if (node.getStart() >= before || !isSchemaParseCall(node)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const arg = firstArgIdentifier(node);
|
|
283
|
+
if (arg !== undefined && names.has(arg)) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
export function isStrictRefinement(before, after) {
|
|
290
|
+
if (before.getText() === after.getText()) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return after.isAssignableTo(before) && !before.isAssignableTo(after);
|
|
294
|
+
}
|
|
295
|
+
export function subjectIdentIn(root, name) {
|
|
296
|
+
if (Node.isIdentifier(root) && root.getText() === name) {
|
|
297
|
+
return root;
|
|
298
|
+
}
|
|
299
|
+
for (const node of root.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
300
|
+
if (node.getText() === name && !declaresName(node)) {
|
|
301
|
+
return node;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
export function conditionNodes(fn) {
|
|
307
|
+
const nodes = [];
|
|
308
|
+
for (const node of ownNodes(fn)) {
|
|
309
|
+
if (Node.isIfStatement(node)) {
|
|
310
|
+
nodes.push(node.getExpression());
|
|
311
|
+
}
|
|
312
|
+
if (Node.isConditionalExpression(node)) {
|
|
313
|
+
nodes.push(node.getCondition());
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return nodes;
|
|
317
|
+
}
|
|
318
|
+
export function truePathRoot(cond) {
|
|
319
|
+
const parent = cond.getParent();
|
|
320
|
+
if (parent === undefined) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const { negated } = splitNegation(unwrapParens(cond));
|
|
324
|
+
if (Node.isIfStatement(parent) && parent.getExpression() === cond) {
|
|
325
|
+
if (negated) {
|
|
326
|
+
return parent.getElseStatement();
|
|
327
|
+
}
|
|
328
|
+
return parent.getThenStatement();
|
|
329
|
+
}
|
|
330
|
+
if (Node.isConditionalExpression(parent) && parent.getCondition() === cond) {
|
|
331
|
+
return negated ? parent.getWhenFalse() : parent.getWhenTrue();
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
function diagnoseAllCallers(fn, cand, cond, negated, sourceFile) {
|
|
336
|
+
const paramIndex = fn.getParameters().findIndex((item) => item.getName() === cand.subject);
|
|
337
|
+
if (paramIndex < 0) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
const calls = sameFileCalls(sourceFile, fn);
|
|
341
|
+
if (calls.length === 0) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
if (!calls.every((call) => argImpliesGuard(call, paramIndex, cand))) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
polarity: negated ? "false" : "true",
|
|
349
|
+
reason: "call-site",
|
|
350
|
+
suggestion: TIGHTEN_OR_DROP,
|
|
351
|
+
reportAt: cond,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function literalConstant(cond) {
|
|
355
|
+
const type = unwrapParens(cond).getType();
|
|
356
|
+
if (!type.isBooleanLiteral()) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
polarity: type.getText() === "true" ? "true" : "false",
|
|
361
|
+
reason: "literal",
|
|
362
|
+
suggestion: DROP_LITERAL,
|
|
363
|
+
reportAt: cond,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: kind dispatch; splitting recreates single-use helpers
|
|
367
|
+
function declaredTypeImplies(param, cand) {
|
|
368
|
+
const typeNode = param.getTypeNode();
|
|
369
|
+
if (typeNode === undefined) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
const text = typeNode.getText().trim();
|
|
373
|
+
if (cand.kind === "typeof") {
|
|
374
|
+
const lit = typeofLiteralFrom(cand.node);
|
|
375
|
+
if (lit === undefined || text.includes("|")) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
return text === lit ? true : undefined;
|
|
379
|
+
}
|
|
380
|
+
if (cand.kind === "nullish") {
|
|
381
|
+
return /\bnull\b|\bundefined\b|\?/.test(text) ? undefined : true;
|
|
382
|
+
}
|
|
383
|
+
if (cand.kind === "predicate" && Node.isCallExpression(cand.node)) {
|
|
384
|
+
const target = predicateTargetType(cand.node);
|
|
385
|
+
const paramType = param.getType();
|
|
386
|
+
if (target === undefined || !isUsableType(target) || !isUsableType(paramType)) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
return paramType.isAssignableTo(target) ? true : undefined;
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
function priorFactImplies(fn, cond, cand) {
|
|
394
|
+
for (const earlier of conditionNodes(fn)) {
|
|
395
|
+
if (earlier.getStart() >= cond.getStart()) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const prev = classifyCondition(fn, earlier);
|
|
399
|
+
if (prev === undefined || !sameGuardShape(prev, cand)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const pathRoot = truePathRoot(earlier);
|
|
403
|
+
if (pathRoot !== undefined && containsNode(pathRoot, cond)) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
function isUsableType(type) {
|
|
410
|
+
return !type.isAny() && !type.isNever() && type.getText() !== "error";
|
|
411
|
+
}
|
|
412
|
+
export function shouldReportUnchanged(cand, before, after) {
|
|
413
|
+
if (cand.kind === "nonempty") {
|
|
414
|
+
return !isNonEmptyType(after);
|
|
415
|
+
}
|
|
416
|
+
if (!isUsableType(before) || !isUsableType(after)) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
if (cand.kind === "instanceof") {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (cand.kind === "predicate") {
|
|
423
|
+
if (!Node.isCallExpression(cand.node)) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const target = predicateTargetType(cand.node);
|
|
427
|
+
if (target !== undefined && isUsableType(target) && before.isAssignableTo(target)) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
return isBareBooleanGuard(cand.node);
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
function isBareBooleanGuard(node) {
|
|
435
|
+
if (!Node.isCallExpression(node)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (hasTypePredicate(node)) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
const signature = node.getProject().getTypeChecker().getResolvedSignature(node);
|
|
442
|
+
if (signature === undefined) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const ret = signature.getReturnType();
|
|
446
|
+
return ret.isBoolean() && !ret.isBooleanLiteral();
|
|
447
|
+
}
|
|
448
|
+
function guardPolarity(subjectType, cand) {
|
|
449
|
+
switch (cand.kind) {
|
|
450
|
+
case "typeof":
|
|
451
|
+
return typeofPolarity(subjectType, cand);
|
|
452
|
+
case "predicate": {
|
|
453
|
+
if (!Node.isCallExpression(cand.node)) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
const target = predicateTargetType(cand.node);
|
|
457
|
+
if (target === undefined) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
return subjectType.isAssignableTo(target) ? true : undefined;
|
|
461
|
+
}
|
|
462
|
+
case "nullish":
|
|
463
|
+
return hasNullish(subjectType) ? undefined : true;
|
|
464
|
+
case "nonempty":
|
|
465
|
+
return isNonEmptyType(subjectType) ? true : undefined;
|
|
466
|
+
case "truthiness":
|
|
467
|
+
return subjectType.isBooleanLiteral() ? subjectType.getText() === "true" : undefined;
|
|
468
|
+
case "instanceof":
|
|
469
|
+
case "schema-parse":
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function typeofPolarity(subjectType, cand) {
|
|
474
|
+
const lit = typeofLiteralFrom(cand.node);
|
|
475
|
+
if (lit === undefined) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
if (typeMatchesTypeof(subjectType, lit)) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
if (typeExcludesTypeof(subjectType, lit)) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
function argImpliesGuard(call, paramIndex, cand) {
|
|
487
|
+
const arg = call.getArguments()[paramIndex];
|
|
488
|
+
if (arg === undefined) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const argType = arg.getType();
|
|
492
|
+
if (!isUsableType(argType)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
return guardPolarity(argType, cand) === true;
|
|
496
|
+
}
|
|
497
|
+
function ownNodes(fn) {
|
|
498
|
+
const nested = fn.getDescendants().filter((node) => isFunctionLike(node));
|
|
499
|
+
const nodes = [];
|
|
500
|
+
for (const node of fn.getDescendants()) {
|
|
501
|
+
if (nested.some((inner) => containsNode(inner, node))) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
nodes.push(node);
|
|
505
|
+
}
|
|
506
|
+
return nodes;
|
|
507
|
+
}
|
|
508
|
+
function sameFileCalls(sourceFile, fn) {
|
|
509
|
+
const parent = fn.getParent();
|
|
510
|
+
const target = Node.isFunctionDeclaration(fn) || Node.isMethodDeclaration(fn)
|
|
511
|
+
? fn.getSymbol()
|
|
512
|
+
: Node.isVariableDeclaration(parent)
|
|
513
|
+
? parent.getNameNode()?.getSymbol()
|
|
514
|
+
: fn.getSymbol();
|
|
515
|
+
if (target === undefined) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
const calls = [];
|
|
519
|
+
for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
520
|
+
if (callMatchesTarget(node, fn, target)) {
|
|
521
|
+
calls.push(node);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return calls;
|
|
525
|
+
}
|
|
526
|
+
function callMatchesTarget(node, fn, target) {
|
|
527
|
+
const expr = node.getExpression();
|
|
528
|
+
if (!Node.isIdentifier(expr)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const symbol = expr.getSymbol();
|
|
532
|
+
if (symbol === undefined) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
const aliased = symbol.getAliasedSymbol() ?? symbol;
|
|
536
|
+
if (symbol !== target &&
|
|
537
|
+
aliased !== target &&
|
|
538
|
+
aliased.getFullyQualifiedName() !== target.getFullyQualifiedName()) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return node.getStart() < fn.getStart() || node.getEnd() > fn.getEnd();
|
|
542
|
+
}
|
|
543
|
+
function wrappingGuard(call, caller, name, expected) {
|
|
544
|
+
let current = call.getParent();
|
|
545
|
+
while (current !== undefined && current !== caller) {
|
|
546
|
+
if (Node.isIfStatement(current)) {
|
|
547
|
+
const cond = current.getExpression();
|
|
548
|
+
const cand = classifyCondition(caller, cond);
|
|
549
|
+
if (cand !== undefined && cand.subject === name && cand.kind === expected.kind) {
|
|
550
|
+
return cond;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
current = current.getParent();
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
function bindingUsedElsewhere(fn, name, ignored) {
|
|
558
|
+
for (const node of fn.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
559
|
+
if (node.getText() !== name || declaresName(node)) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (ignored.some((item) => containsNode(item, node))) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
function containsNode(outer, inner) {
|
|
570
|
+
return inner.getStart() >= outer.getStart() && inner.getEnd() <= outer.getEnd();
|
|
571
|
+
}
|
|
572
|
+
export function enclosingFunction(node) {
|
|
573
|
+
let current = node.getParent();
|
|
574
|
+
while (current !== undefined) {
|
|
575
|
+
if (isFunctionLike(current)) {
|
|
576
|
+
return current;
|
|
577
|
+
}
|
|
578
|
+
current = current.getParent();
|
|
579
|
+
}
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
function sameGuardShape(left, right) {
|
|
583
|
+
if (left.kind !== right.kind || left.subject !== right.subject) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (left.kind === "predicate") {
|
|
587
|
+
return calleeName(left.node) === calleeName(right.node);
|
|
588
|
+
}
|
|
589
|
+
if (left.kind === "typeof") {
|
|
590
|
+
return typeofLiteralFrom(left.node) === typeofLiteralFrom(right.node);
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
function calleeName(node) {
|
|
595
|
+
if (!Node.isCallExpression(node)) {
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
const expr = node.getExpression();
|
|
599
|
+
if (Node.isIdentifier(expr)) {
|
|
600
|
+
return expr.getText();
|
|
601
|
+
}
|
|
602
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
603
|
+
return expr.getName();
|
|
604
|
+
}
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
function hasTypePredicate(call) {
|
|
608
|
+
const checker = call.getProject().getTypeChecker();
|
|
609
|
+
const signature = checker.getResolvedSignature(call);
|
|
610
|
+
if (signature === undefined) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
const typeNode = returnTypeNode(signature.getDeclaration());
|
|
614
|
+
return typeNode !== undefined && Node.isTypePredicate(typeNode);
|
|
615
|
+
}
|
|
616
|
+
function predicateTargetType(call) {
|
|
617
|
+
const checker = call.getProject().getTypeChecker();
|
|
618
|
+
const signature = checker.getResolvedSignature(call);
|
|
619
|
+
if (signature === undefined) {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
const typeNode = returnTypeNode(signature.getDeclaration());
|
|
623
|
+
if (typeNode === undefined || !Node.isTypePredicate(typeNode)) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
return typeNode.getTypeNode()?.getType();
|
|
627
|
+
}
|
|
628
|
+
function returnTypeNode(decl) {
|
|
629
|
+
if (Node.isFunctionDeclaration(decl) ||
|
|
630
|
+
Node.isFunctionExpression(decl) ||
|
|
631
|
+
Node.isArrowFunction(decl) ||
|
|
632
|
+
Node.isMethodDeclaration(decl)) {
|
|
633
|
+
return decl.getReturnTypeNode();
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
function stringLit(node) {
|
|
638
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
639
|
+
return node.getLiteralText();
|
|
640
|
+
}
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
function isNullishLiteral(node) {
|
|
644
|
+
return Node.isNullLiteral(node) || (Node.isIdentifier(node) && node.getText() === "undefined");
|
|
645
|
+
}
|
|
646
|
+
function objectVsNullish(left, right) {
|
|
647
|
+
const access = Node.isPropertyAccessExpression(left)
|
|
648
|
+
? left
|
|
649
|
+
: Node.isPropertyAccessExpression(right)
|
|
650
|
+
? right
|
|
651
|
+
: undefined;
|
|
652
|
+
if (access === undefined) {
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
const object = unwrapParens(access.getExpression());
|
|
656
|
+
return Node.isIdentifier(object) ? object.getText() : undefined;
|
|
657
|
+
}
|
|
658
|
+
function nonEmptySubject(node) {
|
|
659
|
+
if (!Node.isBinaryExpression(node)) {
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
const op = node.getOperatorToken().getText();
|
|
663
|
+
const left = unwrapParens(node.getLeft());
|
|
664
|
+
const right = unwrapParens(node.getRight());
|
|
665
|
+
if (!Node.isNumericLiteral(right) || !Node.isPropertyAccessExpression(left)) {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
if (left.getName() !== "length") {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
const object = unwrapParens(left.getExpression());
|
|
672
|
+
if (!Node.isIdentifier(object)) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
if (!looksLikeArrayBinding(object)) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
const num = Number(right.getLiteralText());
|
|
679
|
+
const isNonZero = (op === "===" || op === "==" || op === "!==" || op === "!=") && num === 0 && op.startsWith("!");
|
|
680
|
+
if ((op === ">" && num === 0) || (op === ">=" && num === 1) || isNonZero) {
|
|
681
|
+
return object.getText();
|
|
682
|
+
}
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
function typeofLiteralFrom(node) {
|
|
686
|
+
if (!Node.isBinaryExpression(node)) {
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
return stringLit(unwrapParens(node.getLeft())) ?? stringLit(unwrapParens(node.getRight()));
|
|
690
|
+
}
|
|
691
|
+
function typeMatchesTypeof(type, lit) {
|
|
692
|
+
if (type.isUnion()) {
|
|
693
|
+
return type.getUnionTypes().every((part) => typeMatchesTypeof(part, lit));
|
|
694
|
+
}
|
|
695
|
+
if (lit === "string") {
|
|
696
|
+
return type.isString();
|
|
697
|
+
}
|
|
698
|
+
if (lit === "number") {
|
|
699
|
+
return type.isNumber();
|
|
700
|
+
}
|
|
701
|
+
if (lit === "boolean") {
|
|
702
|
+
return type.isBoolean() || type.isBooleanLiteral();
|
|
703
|
+
}
|
|
704
|
+
if (lit === "undefined") {
|
|
705
|
+
return type.isUndefined();
|
|
706
|
+
}
|
|
707
|
+
if (lit === "object") {
|
|
708
|
+
return type.isNull() || type.isObject() || type.isClassOrInterface();
|
|
709
|
+
}
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
function typeExcludesTypeof(type, lit) {
|
|
713
|
+
if (type.isUnion()) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
if (lit === "string") {
|
|
717
|
+
return (type.isNumber() ||
|
|
718
|
+
type.isBoolean() ||
|
|
719
|
+
type.isBooleanLiteral() ||
|
|
720
|
+
type.isUndefined() ||
|
|
721
|
+
type.isNull());
|
|
722
|
+
}
|
|
723
|
+
if (lit === "number") {
|
|
724
|
+
return (type.isString() ||
|
|
725
|
+
type.isBoolean() ||
|
|
726
|
+
type.isBooleanLiteral() ||
|
|
727
|
+
type.isUndefined() ||
|
|
728
|
+
type.isNull());
|
|
729
|
+
}
|
|
730
|
+
if (lit === "boolean") {
|
|
731
|
+
return type.isString() || type.isNumber() || type.isUndefined() || type.isNull();
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
function hasNullish(type) {
|
|
736
|
+
const parts = type.isUnion() ? type.getUnionTypes() : [type];
|
|
737
|
+
return parts.some((part) => part.isNull() || part.isUndefined());
|
|
738
|
+
}
|
|
739
|
+
function isNonEmptyType(type) {
|
|
740
|
+
return type.isTuple() && type.getTupleElements().length > 0;
|
|
741
|
+
}
|
|
742
|
+
function looksLikeArrayBinding(ident) {
|
|
743
|
+
if (!Node.isIdentifier(ident)) {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
const type = ident.getType();
|
|
747
|
+
if (type.isArray() || type.isTuple()) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
if (type.isString() || type.isNumber() || type.isBoolean()) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
const symbol = ident.getSymbol();
|
|
754
|
+
const decl = symbol?.getDeclarations()[0];
|
|
755
|
+
if (decl === undefined) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
if (Node.isParameterDeclaration(decl) || Node.isVariableDeclaration(decl)) {
|
|
759
|
+
const text = decl.getTypeNode()?.getText() ?? "";
|
|
760
|
+
return text.includes("[]") || text.includes("Array<");
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
function declaresName(node) {
|
|
765
|
+
const parent = node.getParent();
|
|
766
|
+
if (parent === undefined) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
if (Node.isParameterDeclaration(parent) || Node.isVariableDeclaration(parent)) {
|
|
770
|
+
const nameNode = parent.getNameNode();
|
|
771
|
+
return nameNode !== undefined && nameNode.getStart() === node.getStart();
|
|
772
|
+
}
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
export { isFunctionLike };
|
|
776
|
+
//# sourceMappingURL=narrowing.js.map
|