Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 16 additions & 23 deletions src/rules/executor-cancel-wiring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
/** True when the constructor options literally set `shield: true`: a shielded promise ignores cancel. */
function isShielded(node: TSESTree.NewExpression, context: TSESLint.RuleContext<MessageIds, RuleOptions>): boolean {
const options = node.arguments[1];
if (!options) {

Check warning on line 27 in src/rules/executor-cancel-wiring.ts

View workflow job for this annotation

GitHub Actions / check

Unnecessary conditional, value is always falsy

Check warning on line 27 in src/rules/executor-cancel-wiring.ts

View workflow job for this annotation

GitHub Actions / check

Unnecessary conditional, value is always falsy
return false;
}
let objExpr: TSESTree.ObjectExpression | null = null;
Expand All @@ -44,32 +44,25 @@
return false;
}

let foundShieldIndex = -1;
for (let i = 0; i < objExpr.properties.length; i++) {
const prop = objExpr.properties[i];
if (
prop.type === AST_NODE_TYPES.Property &&
!prop.computed &&
((prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === 'shield') ||
(prop.key.type === AST_NODE_TYPES.Literal && prop.key.value === 'shield')) &&
prop.value.type === AST_NODE_TYPES.Literal &&
prop.value.value === true
) {
foundShieldIndex = i;
let shielded = false;
for (const prop of objExpr.properties) {
if (prop.type === AST_NODE_TYPES.SpreadElement) {
shielded = false;
continue;
}
}

if (foundShieldIndex === -1) {
return false;
}

for (let i = foundShieldIndex + 1; i < objExpr.properties.length; i++) {
if (objExpr.properties[i].type === AST_NODE_TYPES.SpreadElement) {
return false;
if (prop.computed) {
shielded = false;
continue;
}
const isShieldKey =
(prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === 'shield') ||
(prop.key.type === AST_NODE_TYPES.Literal && prop.key.value === 'shield');
if (isShieldKey) {
const val = unwrapTypeOnly(prop.value);
shielded = val.type === AST_NODE_TYPES.Literal && val.value === true;
}
}

return true;
return shielded;
}

export default createRule<RuleOptions, MessageIds>({
Expand Down
82 changes: 55 additions & 27 deletions src/rules/exhaustive-catch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import createRule from '../util/create-rule.js';
import { unwrapTypeOnly } from '../util/executor.js';
import { analyzeCatchSteps, type CatchStep, isTypePredicateCall } from '../util/failure-checker-exhaustive-catch.js';
import { collectHandledCatchTypes } from '../util/failure-types.js';
import { collectHandledCatchTypes, collectHandledSwitchLabels } from '../util/failure-types.js';

export type MessageIds = 'unhandledFailure' | 'noRethrow' | 'missingUnlistedFallback';

Expand Down Expand Up @@ -93,29 +93,22 @@
if (rethrowAt === -1) {
return keptGuardTypes(body, bindingName, context);
}
const handled: ts.Type[] = [];
for (const stmt of body.slice(0, rethrowAt)) {
if (
stmt.type === AST_NODE_TYPES.IfStatement &&
!stmt.alternate &&
isRecognizedGuard(stmt.test, bindingName, context) &&
endsWithExit(stmt.consequent)
) {
// the collector walks any node; only the guard test names the kept types
handled.push(...collectHandledCatchTypes(stmt.test, bindingName, context));
}
}
return handled;
return keptGuardTypes(body.slice(0, rethrowAt), bindingName, context, true);
}

/**
* Guard types of the branches that keep the binding: the branch never rethrows it, and either exits or no later
* statement can rethrow it.
*/
function keptGuardTypes(body: TSESTree.Statement[], bindingName: string, context: Context): ts.Type[] {
function keptGuardTypes(
body: TSESTree.Statement[],
bindingName: string,
context: Context,
trailingRethrow = false,
): ts.Type[] {
const handled: ts.Type[] = [];
body.forEach((stmt, i) => {
const laterRethrow = body.slice(i + 1).some((s) => subtreeRethrows(s, bindingName, context));
const laterRethrow = trailingRethrow || body.slice(i + 1).some((s) => subtreeRethrows(s, bindingName, context));
const keep = (
test: TSESTree.Expression,
branch: TSESTree.Statement[],
Expand Down Expand Up @@ -148,27 +141,54 @@
branch = branch.alternate;
}
} else if (stmt.type === AST_NODE_TYPES.SwitchStatement) {
const guarded = hasGuardCases(stmt, bindingName, context);
stmt.cases.forEach((c, at) => {
if (c.test) {
// a case without break falls through, so the following cases run as part of it
keep(c.test, fallThroughBody(stmt.cases.slice(at)), guarded, isReturn);
}
});
if (isBindingMember(stmt.discriminant, bindingName)) {
const keptLabels = new Set<string>();
stmt.cases.forEach((c, at) => {
if (c.test?.type === AST_NODE_TYPES.Literal && typeof c.test.value === 'string') {
const branch = fallThroughBody(stmt.cases.slice(at));
const last = branch.at(-1);
if (
!branch.some((s) => subtreeRethrows(s, bindingName, context)) &&
(!laterRethrow || (last !== undefined && isReturn(last)))
) {
keptLabels.add(c.test.value);
}
}
});
handled.push(...collectHandledSwitchLabels(stmt, bindingName, keptLabels, context));
} else {
const guarded = hasGuardCases(stmt, bindingName, context);
stmt.cases.forEach((c, at) => {
if (c.test) {
keep(c.test, fallThroughBody(stmt.cases.slice(at)), guarded, isReturn);
}
});
}
}
});
return handled;
}

function trailingStmt(stmt: TSESTree.Statement | undefined): TSESTree.Statement | undefined {
let cur = stmt;
while (cur?.type === AST_NODE_TYPES.BlockStatement) {
cur = cur.body.at(-1);
}
return cur;
}

/** Statements a switch case runs, through the following cases until one ends with a jump. */
function fallThroughBody(cases: TSESTree.SwitchCase[]): TSESTree.Statement[] {
const run: TSESTree.Statement[] = [];
for (const c of cases) {
run.push(...c.consequent);
const last = c.consequent.at(-1);
const last = trailingStmt(c.consequent.at(-1));
if (
last &&
(last.type === AST_NODE_TYPES.BreakStatement || isReturn(last) || last.type === AST_NODE_TYPES.ThrowStatement)
(last.type === AST_NODE_TYPES.BreakStatement ||
last.type === AST_NODE_TYPES.ContinueStatement ||
last.type === AST_NODE_TYPES.ReturnStatement ||
last.type === AST_NODE_TYPES.ThrowStatement)
) {
break;
}
Expand All @@ -177,7 +197,7 @@
}

function isReturn(stmt: TSESTree.Statement): boolean {
return stmt.type === AST_NODE_TYPES.ReturnStatement;
return trailingStmt(stmt)?.type === AST_NODE_TYPES.ReturnStatement;
}

/** Whether the binding is rethrown anywhere under the statement, outside nested functions. */
Expand All @@ -198,7 +218,7 @@
}

function endsWithExit(stmt: TSESTree.Statement): boolean {
const last = stmt.type === AST_NODE_TYPES.BlockStatement ? stmt.body.at(-1) : stmt;
const last = trailingStmt(stmt);
return (
last?.type === AST_NODE_TYPES.ReturnStatement ||
last?.type === AST_NODE_TYPES.BreakStatement ||
Expand Down Expand Up @@ -243,7 +263,7 @@
return;
}

if (node.type === AST_NODE_TYPES.YieldExpression && node.delegate && node.argument) {

Check warning on line 266 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Unnecessary conditional, value is always truthy

Check warning on line 266 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Unnecessary conditional, value is always truthy
steps.push({ step: node, innerHandled });
return;
}
Expand All @@ -256,7 +276,7 @@
}

function isRethrowStatement(stmt: TSESTree.Statement, bindingName: string, context: Context): boolean {
if (stmt.type === AST_NODE_TYPES.ThrowStatement && stmt.argument) {

Check warning on line 279 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Unnecessary conditional, value is always truthy

Check warning on line 279 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Unnecessary conditional, value is always truthy
const arg = unwrapTypeOnly(stmt.argument);
if (arg.type === AST_NODE_TYPES.Identifier && arg.name === bindingName) {
return true;
Expand Down Expand Up @@ -424,6 +444,14 @@
// an exit under a guard leaves only for the errors the guard matches
return node.alternate ? hasSwallowingExit(node.alternate, bindingName, context, nested) : false;
}
if (node.type === AST_NODE_TYPES.SwitchStatement && hasGuardCases(node, bindingName, context)) {
const defaultCase = node.cases.find((c) => c.test === null);
if (!defaultCase) {
return false;
}
const defaultAt = node.cases.indexOf(defaultCase);
return fallThroughBody(node.cases.slice(defaultAt)).some((s) => hasSwallowingExit(s, bindingName, context, true));
}
const childNested = nested || LOOP_OR_SWITCH.has(node.type);
let found = false;
forEachChild(node, (child) => {
Expand Down Expand Up @@ -464,7 +492,7 @@
if (stmt.alternate) {
let currAlt: TSESTree.Statement | null = stmt.alternate;
let allNarrowed = true;
while (currAlt && currAlt.type === AST_NODE_TYPES.IfStatement) {

Check warning on line 495 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Prefer using an optional chain expression instead, as it's more concise and easier to read

Check warning on line 495 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Prefer using an optional chain expression instead, as it's more concise and easier to read
if (!isRecognizedGuard(currAlt.test, bindingName, context)) {
allNarrowed = false;
break;
Expand Down Expand Up @@ -585,7 +613,7 @@
}

const bindingName =
node.handler.param && node.handler.param.type === AST_NODE_TYPES.Identifier ?

Check warning on line 616 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Prefer using an optional chain expression instead, as it's more concise and easier to read

Check warning on line 616 in src/rules/exhaustive-catch.ts

View workflow job for this annotation

GitHub Actions / check

Prefer using an optional chain expression instead, as it's more concise and easier to read
node.handler.param.name
: undefined;

Expand Down
9 changes: 1 addition & 8 deletions src/rules/no-yield-await-in-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,14 +589,7 @@ export default createRule<RuleOptions, MessageIds>({
return;
}

let arg: TSESTree.Node = node.argument;
while (
arg.type === AST_NODE_TYPES.TSAsExpression ||
arg.type === AST_NODE_TYPES.TSTypeAssertion ||
arg.type === AST_NODE_TYPES.TSNonNullExpression
) {
arg = arg.expression;
}
const arg = unwrapTypeOnly(node.argument);

if (arg.type !== AST_NODE_TYPES.CallExpression) {
return;
Expand Down
Loading
Loading