diff --git a/eslint.config.mjs b/eslint.config.mjs index c87098636b74..6e510fb45010 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -129,7 +129,7 @@ export default tseslint.config( 'no-constant-condition': 'off', '@typescript-eslint/no-unnecessary-condition': [ 'error', - { allowConstantLoopConditions: true }, + { allowConstantLoopConditions: true, checkTypePredicates: true }, ], '@typescript-eslint/no-unnecessary-type-parameters': 'error', '@typescript-eslint/no-unused-expressions': 'error', diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.mdx index ec5f234ac981..befeea8b2401 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.mdx @@ -90,6 +90,46 @@ for (; true; ) {} do {} while (true); ``` +### `checkTypePredicates` + +Example of additional incorrect code with `{ checkTypePredicates: true }`: + +```ts option='{ "checkTypePredicates": true }' showPlaygroundButton +function assert(condition: unknown): asserts condition { + if (!condition) { + throw new Error('Condition is falsy'); + } +} + +assert(false); // Unnecessary; condition is always falsy. + +const neverNull = {}; +assert(neverNull); // Unnecessary; condition is always truthy. + +function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +declare const s: string; + +// Unnecessary; s is always a string. +if (isString(s)) { +} + +function assertIsString(value: unknown): asserts value is string { + if (!isString(value)) { + throw new Error('Value is not a string'); + } +} + +assertIsString(s); // Unnecessary; s is always a string. +``` + +Whether this option makes sense for your project may vary. +Some projects may intentionally use type predicates to ensure that runtime values do indeed match the types according to TypeScript, especially in test code. +Often, it makes sense to use eslint-disable comments in these cases, with a comment indicating why the condition should be checked at runtime, despite appearing unnecessary. +However, in some contexts, it may be more appropriate to keep this option disabled entirely. + ### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing` :::danger Deprecated diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 035ba000f33d..4befec262769 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -18,6 +18,10 @@ import { nullThrows, NullThrowsReasons, } from '../util'; +import { + findTruthinessAssertedArgument, + findTypeGuardAssertedArgument, +} from '../util/assertionFunctionUtils'; // Truthiness utilities // #region @@ -71,6 +75,7 @@ export type Options = [ { allowConstantLoopConditions?: boolean; allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; + checkTypePredicates?: boolean; }, ]; @@ -81,6 +86,8 @@ export type MessageId = | 'alwaysTruthy' | 'alwaysTruthyFunc' | 'literalBooleanExpression' + | 'typeGuardAlreadyIsType' + | 'replaceWithTrue' | 'never' | 'neverNullish' | 'neverOptionalChain' @@ -111,6 +118,11 @@ export default createRule({ 'Whether to not error when running with a tsconfig that has strictNullChecks turned.', type: 'boolean', }, + checkTypePredicates: { + description: + 'Whether to check the asserted argument of a type predicate function for unnecessary conditions', + type: 'boolean', + }, }, additionalProperties: false, }, @@ -129,18 +141,22 @@ export default createRule({ 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.', literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values.', + replaceWithTrue: 'Replace always true expression with `true`.', noOverlapBooleanExpression: 'Unnecessary conditional, the types have no overlap.', never: 'Unnecessary conditional, value is `never`.', neverOptionalChain: 'Unnecessary optional chain on a non-nullish value.', noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', + typeGuardAlreadyIsType: + 'Unnecessary conditional, expression already has the type being checked by the {{typeGuardOrAssertionFunction}}.', }, }, defaultOptions: [ { allowConstantLoopConditions: false, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + checkTypePredicates: false, }, ], create( @@ -149,6 +165,7 @@ export default createRule({ { allowConstantLoopConditions, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, + checkTypePredicates, }, ], ) { @@ -463,6 +480,38 @@ export default createRule({ } function checkCallExpression(node: TSESTree.CallExpression): void { + if (checkTypePredicates) { + const truthinessAssertedArgument = findTruthinessAssertedArgument( + services, + node, + ); + if (truthinessAssertedArgument != null) { + checkNode(truthinessAssertedArgument); + } + + const typeGuardAssertedArgument = findTypeGuardAssertedArgument( + services, + node, + ); + if (typeGuardAssertedArgument != null) { + const typeOfArgument = getConstrainedTypeAtLocation( + services, + typeGuardAssertedArgument.argument, + ); + if (typeOfArgument === typeGuardAssertedArgument.type) { + context.report({ + node: typeGuardAssertedArgument.argument, + messageId: 'typeGuardAlreadyIsType', + data: { + typeGuardOrAssertionFunction: typeGuardAssertedArgument.asserts + ? 'assertion function' + : 'type guard', + }, + }); + } + } + } + // If this is something like arr.filter(x => /*condition*/), check `condition` if ( isArrayMethodCallWithPredicate(context, services, node) && diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 1e979e47a4c8..cea09251e53f 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -1,7 +1,6 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { RuleFix } from '@typescript-eslint/utils/ts-eslint'; -import * as ts from 'typescript'; import { createRule, @@ -130,14 +129,10 @@ export default createRule< function isLeftSideLowerPrecedence(): boolean { const logicalTsNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const leftTsNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode); - const operator = ts.isBinaryExpression(logicalTsNode) - ? logicalTsNode.operatorToken.kind - : ts.SyntaxKind.Unknown; const leftPrecedence = getOperatorPrecedence( leftTsNode.kind, - operator, + logicalTsNode.operatorToken.kind, ); return leftPrecedence < OperatorPrecedence.LeftHandSide; diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index d65fd9736ad5..ca0bd74c03bd 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -535,11 +535,7 @@ export default createRule({ const callNode = getParent(node) as TSESTree.CallExpression; const parentNode = getParent(callNode) as TSESTree.BinaryExpression; - if ( - !isEqualityComparison(parentNode) || - !isNull(parentNode.right) || - !isStringType(node.object) - ) { + if (!isNull(parentNode.right) || !isStringType(node.object)) { return; } diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 7bd708ac6f9c..7adbad1e525d 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -13,6 +13,7 @@ import { getWrappingFixer, isTypeArrayTypeOrUnionOfArrayTypes, } from '../util'; +import { findTruthinessAssertedArgument } from '../util/assertionFunctionUtils'; export type Options = [ { @@ -267,134 +268,12 @@ export default createRule({ } function traverseCallExpression(node: TSESTree.CallExpression): void { - const assertedArgument = findAssertedArgument(node); + const assertedArgument = findTruthinessAssertedArgument(services, node); if (assertedArgument != null) { traverseNode(assertedArgument, true); } } - /** - * Inspect a call expression to see if it's a call to an assertion function. - * If it is, return the node of the argument that is asserted. - */ - function findAssertedArgument( - node: TSESTree.CallExpression, - ): TSESTree.Expression | undefined { - // If the call looks like `assert(expr1, expr2, ...c, d, e, f)`, then we can - // only care if `expr1` or `expr2` is asserted, since anything that happens - // within or after a spread argument is out of scope to reason about. - const checkableArguments: TSESTree.Expression[] = []; - for (const argument of node.arguments) { - if (argument.type === AST_NODE_TYPES.SpreadElement) { - break; - } - - checkableArguments.push(argument); - } - - // nothing to do - if (checkableArguments.length === 0) { - return undefined; - } - - // Game plan: we're going to check the type of the callee. If it has call - // signatures and they _ALL_ agree that they assert on a parameter at the - // _SAME_ position, we'll consider the argument in that position to be an - // asserted argument. - const calleeType = getConstrainedTypeAtLocation(services, node.callee); - const callSignatures = tsutils.getCallSignaturesOfType(calleeType); - - let assertedParameterIndex: number | undefined = undefined; - for (const signature of callSignatures) { - const declaration = signature.getDeclaration(); - const returnTypeAnnotation = declaration.type; - - // Be sure we're dealing with a truthiness assertion function. - if ( - !( - returnTypeAnnotation != null && - ts.isTypePredicateNode(returnTypeAnnotation) && - // This eliminates things like `x is string` and `asserts x is T` - // leaving us with just the `asserts x` cases. - returnTypeAnnotation.type == null && - // I think this is redundant but, still, it needs to be true - returnTypeAnnotation.assertsModifier != null - ) - ) { - return undefined; - } - - const assertionTarget = returnTypeAnnotation.parameterName; - if (assertionTarget.kind !== ts.SyntaxKind.Identifier) { - // This can happen when asserting on `this`. Ignore! - return undefined; - } - - // If the first parameter is `this`, skip it, so that our index matches - // the index of the argument at the call site. - const firstParameter = declaration.parameters.at(0); - const nonThisParameters = - firstParameter?.name.kind === ts.SyntaxKind.Identifier && - firstParameter.name.text === 'this' - ? declaration.parameters.slice(1) - : declaration.parameters; - - // Don't bother inspecting parameters past the number of - // arguments we have at the call site. - const checkableNonThisParameters = nonThisParameters.slice( - 0, - checkableArguments.length, - ); - - let assertedParameterIndexForThisSignature: number | undefined; - for (const [index, parameter] of checkableNonThisParameters.entries()) { - if (parameter.dotDotDotToken != null) { - // Cannot assert a rest parameter, and can't have a rest parameter - // before the asserted parameter. It's not only a TS error, it's - // not something we can logically make sense of, so give up here. - return undefined; - } - - if (parameter.name.kind !== ts.SyntaxKind.Identifier) { - // Only identifiers are valid for assertion targets, so skip over - // anything like `{ destructuring: parameter }: T` - continue; - } - - // we've found a match between the "target"s in - // `function asserts(target: T): asserts target;` - if (parameter.name.text === assertionTarget.text) { - assertedParameterIndexForThisSignature = index; - break; - } - } - - if (assertedParameterIndexForThisSignature == null) { - // Didn't find an assertion target in this signature that could match - // the call site. - return undefined; - } - - if ( - assertedParameterIndex != null && - assertedParameterIndex !== assertedParameterIndexForThisSignature - ) { - // The asserted parameter we found for this signature didn't match - // previous signatures. - return undefined; - } - - assertedParameterIndex = assertedParameterIndexForThisSignature; - } - - // Didn't find a unique assertion index. - if (assertedParameterIndex == null) { - return undefined; - } - - return checkableArguments[assertedParameterIndex]; - } - /** * Inspects any node. * diff --git a/packages/eslint-plugin/src/util/assertionFunctionUtils.ts b/packages/eslint-plugin/src/util/assertionFunctionUtils.ts new file mode 100644 index 000000000000..c561d1c59df8 --- /dev/null +++ b/packages/eslint-plugin/src/util/assertionFunctionUtils.ts @@ -0,0 +1,119 @@ +import type { + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as ts from 'typescript'; + +/** + * Inspect a call expression to see if it's a call to an assertion function. + * If it is, return the node of the argument that is asserted. + */ +export function findTruthinessAssertedArgument( + services: ParserServicesWithTypeInformation, + node: TSESTree.CallExpression, +): TSESTree.Expression | undefined { + // If the call looks like `assert(expr1, expr2, ...c, d, e, f)`, then we can + // only care if `expr1` or `expr2` is asserted, since anything that happens + // within or after a spread argument is out of scope to reason about. + const checkableArguments: TSESTree.Expression[] = []; + for (const argument of node.arguments) { + if (argument.type === AST_NODE_TYPES.SpreadElement) { + break; + } + checkableArguments.push(argument); + } + + // nothing to do + if (checkableArguments.length === 0) { + return undefined; + } + + const checker = services.program.getTypeChecker(); + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + const signature = checker.getResolvedSignature(tsNode); + + if (signature == null) { + return undefined; + } + + const firstTypePredicateResult = + checker.getTypePredicateOfSignature(signature); + + if (firstTypePredicateResult == null) { + return undefined; + } + + const { parameterIndex, kind, type } = firstTypePredicateResult; + if (!(kind === ts.TypePredicateKind.AssertsIdentifier && type == null)) { + return undefined; + } + + return checkableArguments.at(parameterIndex); +} + +/** + * Inspect a call expression to see if it's a call to an assertion function. + * If it is, return the node of the argument that is asserted and other useful info. + */ +export function findTypeGuardAssertedArgument( + services: ParserServicesWithTypeInformation, + node: TSESTree.CallExpression, +): + | { + asserts: boolean; + argument: TSESTree.Expression; + type: ts.Type; + } + | undefined { + // If the call looks like `assert(expr1, expr2, ...c, d, e, f)`, then we can + // only care if `expr1` or `expr2` is asserted, since anything that happens + // within or after a spread argument is out of scope to reason about. + const checkableArguments: TSESTree.Expression[] = []; + for (const argument of node.arguments) { + if (argument.type === AST_NODE_TYPES.SpreadElement) { + break; + } + checkableArguments.push(argument); + } + + // nothing to do + if (checkableArguments.length === 0) { + return undefined; + } + + const checker = services.program.getTypeChecker(); + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + const callSignature = checker.getResolvedSignature(tsNode); + + if (callSignature == null) { + return undefined; + } + + const typePredicateInfo = checker.getTypePredicateOfSignature(callSignature); + + if (typePredicateInfo == null) { + return undefined; + } + + const { parameterIndex, kind, type } = typePredicateInfo; + if ( + !( + (kind === ts.TypePredicateKind.AssertsIdentifier || + kind === ts.TypePredicateKind.Identifier) && + type != null + ) + ) { + return undefined; + } + + if (parameterIndex >= checkableArguments.length) { + return undefined; + } + + return { + type, + asserts: kind === ts.TypePredicateKind.AssertsIdentifier, + argument: checkableArguments[parameterIndex], + }; +} diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-condition.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-condition.shot index 508ffbe028f6..3ec0af465b4f 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-condition.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-condition.shot @@ -66,3 +66,41 @@ for (; true; ) {} do {} while (true); " `; + +exports[`Validating rule docs no-unnecessary-condition.mdx code examples ESLint output 4`] = ` +"Options: { "checkTypePredicates": true } + +function assert(condition: unknown): asserts condition { + if (!condition) { + throw new Error('Condition is falsy'); + } +} + +assert(false); // Unnecessary; condition is always falsy. + ~~~~~ Unnecessary conditional, value is always falsy. + +const neverNull = {}; +assert(neverNull); // Unnecessary; condition is always truthy. + ~~~~~~~~~ Unnecessary conditional, value is always truthy. + +function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +declare const s: string; + +// Unnecessary; s is always a string. +if (isString(s)) { + ~ Unnecessary conditional, expression already has the type being checked by the type guard. +} + +function assertIsString(value: unknown): asserts value is string { + if (!isString(value)) { + throw new Error('Value is not a string'); + } +} + +assertIsString(s); // Unnecessary; s is always a string. + ~ Unnecessary conditional, expression already has the type being checked by the assertion function. +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 9fbd0eb7ac6f..91e770cefef9 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -894,7 +894,103 @@ class ConsistentRand { } `, }, + { + code: ` +declare function assert(x: unknown): asserts x; + +assert(Math.random() > 0.5); + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare function assert(x: unknown, y: unknown): asserts x; + +assert(Math.random() > 0.5, true); + `, + options: [{ checkTypePredicates: true }], + }, + { + // should not report because option is disabled. + code: ` +declare function assert(x: unknown): asserts x; +assert(true); + `, + options: [{ checkTypePredicates: false }], + }, + { + // could be argued that this should report since `thisAsserter` is truthy. + code: ` +class ThisAsserter { + assertThis(this: unknown, arg2: unknown): asserts this {} +} + +const thisAsserter: ThisAsserter = new ThisAsserter(); +thisAsserter.assertThis(true); + `, + options: [{ checkTypePredicates: true }], + }, + { + // could be argued that this should report since `thisAsserter` is truthy. + code: ` +class ThisAsserter { + assertThis(this: unknown, arg2: unknown): asserts this {} +} + +const thisAsserter: ThisAsserter = new ThisAsserter(); +thisAsserter.assertThis(Math.random()); + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare function assert(x: unknown): asserts x; +assert(...[]); + `, + options: [{ checkTypePredicates: true }], + }, + { + // ok to report if we start unpacking spread params one day. + code: ` +declare function assert(x: unknown): asserts x; +assert(...[], {}); + `, + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare function assertString(x: unknown): asserts x is string; +declare const a: string; +assertString(a); + `, + options: [{ checkTypePredicates: false }], + }, + { + code: ` +declare function isString(x: unknown): x is string; +declare const a: string; +isString(a); + `, + options: [{ checkTypePredicates: false }], + }, + { + // Technically, this has type 'falafel' and not string. + code: ` +declare function assertString(x: unknown): asserts x is string; +assertString('falafel'); + `, + options: [{ checkTypePredicates: true }], + }, + { + // Technically, this has type 'falafel' and not string. + code: ` +declare function isString(x: unknown): x is string; +isString('falafel'); + `, + options: [{ checkTypePredicates: true }], + }, ], + invalid: [ // Ensure that it's checking in all the right places { @@ -2305,6 +2401,104 @@ foo?.['bar']?.().toExponential(); `, errors: [ruleError(3, 13, 'alwaysTruthy')], }, + { + code: ` +declare function assert(x: unknown): asserts x; +assert(true); + `, + errors: [ + { + line: 3, + messageId: 'alwaysTruthy', + }, + ], + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare function assert(x: unknown): asserts x; +assert(false); + `, + errors: [ + { + line: 3, + column: 8, + messageId: 'alwaysFalsy', + }, + ], + options: [{ checkTypePredicates: true }], + }, + { + code: ` +declare function assert(x: unknown, y: unknown): asserts x; + +assert(true, Math.random() > 0.5); + `, + options: [{ checkTypePredicates: true }], + errors: [ + { + messageId: 'alwaysTruthy', + line: 4, + column: 8, + }, + ], + }, + { + code: ` +declare function assert(x: unknown): asserts x; +assert({}); + `, + options: [{ checkTypePredicates: true }], + errors: [ + { + messageId: 'alwaysTruthy', + line: 3, + column: 8, + }, + ], + }, + { + code: ` +declare function assertsString(x: unknown): asserts x is string; +declare const a: string; +assertsString(a); + `, + options: [{ checkTypePredicates: true }], + errors: [ + { + messageId: 'typeGuardAlreadyIsType', + line: 4, + }, + ], + }, + { + code: ` +declare function isString(x: unknown): x is string; +declare const a: string; +isString(a); + `, + options: [{ checkTypePredicates: true }], + errors: [ + { + messageId: 'typeGuardAlreadyIsType', + line: 4, + }, + ], + }, + { + code: ` +declare function isString(x: unknown): x is string; +declare const a: string; +isString('fa' + 'lafel'); + `, + options: [{ checkTypePredicates: true }], + errors: [ + { + messageId: 'typeGuardAlreadyIsType', + line: 4, + }, + ], + }, // "branded" types unnecessaryConditionTest('"" & {}', 'alwaysFalsy'), diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index 6706f6e10c6b..50395575a81f 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -439,14 +439,6 @@ assert(boo, nullableString); declare function assert(a: boolean, b: unknown): asserts b is string; declare const nullableString: string | null; declare const boo: boolean; -assert(boo, nullableString); - `, - // Intentional TS error - cannot assert a parameter in a binding pattern. - ` -declare function assert(a: boolean, b: unknown): asserts b; -declare function assert(a: boolean, { b }: { b: unknown }): asserts b; -declare const nullableString: string | null; -declare const boo: boolean; assert(boo, nullableString); `, ` @@ -516,39 +508,6 @@ declare const nullableString: string | null; assert(3 as any, nullableString); `, }, - // Intentional TS error - A rest parameter must be last in a parameter list. - // This is just to test that we don't crash or falsely report. - ` -declare function assert(...a: boolean[], b: unknown): asserts b; -declare const nullableString: string | null; -declare const boo: boolean; -assert(boo, nullableString); - `, - // Intentional TS error - A type predicate cannot reference a rest parameter. - // This is just to test that we don't crash or falsely report. - ` -declare function assert(a: boolean, ...b: unknown[]): asserts b; -declare const nullableString: string | null; -declare const boo: boolean; -assert(boo, nullableString); - `, - // Intentional TS error - An assertion function must have a parameter to assert. - // This is just to test that we don't crash or falsely report. - ` -declare function assert(): asserts x; -declare const nullableString: string | null; -assert(nullableString); - `, - ` -function assert(one: unknown): asserts one; -function assert(one: unknown, two: unknown): asserts two; -function assert(...args: unknown[]) { - throw new Error('not implemented'); -} -declare const nullableString: string | null; -assert(nullableString); -assert('one', nullableString); - `, // Intentional use of `any` to test a function call with no call signatures. ` declare const assert: any; @@ -2578,6 +2537,12 @@ assert(foo, Boolean(nullableString)); ], }, { + // This should be checkable, but the TS API doesn't currently report + // `someAssert(maybeString)` as a type predicate call, which appears to be + // a bug. + // + // See https://github.com/microsoft/TypeScript/issues/59707 + skip: true, code: ` function asserts1(x: string | number | undefined): asserts x {} function asserts2(x: string | number | undefined): asserts x {} @@ -2889,5 +2854,61 @@ assert(boo, Boolean(nullableString)); }, ], }, + { + // This report matches TS's analysis, which selects the assertion overload. + code: ` +function assert(one: unknown): asserts one; +function assert(one: unknown, two: unknown): asserts two; +function assert(...args: unknown[]) { + throw new Error('not implemented'); +} +declare const nullableString: string | null; +assert(nullableString); + `, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 8, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +function assert(one: unknown): asserts one; +function assert(one: unknown, two: unknown): asserts two; +function assert(...args: unknown[]) { + throw new Error('not implemented'); +} +declare const nullableString: string | null; +assert(nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +function assert(one: unknown): asserts one; +function assert(one: unknown, two: unknown): asserts two; +function assert(...args: unknown[]) { + throw new Error('not implemented'); +} +declare const nullableString: string | null; +assert(nullableString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +function assert(one: unknown): asserts one; +function assert(one: unknown, two: unknown): asserts two; +function assert(...args: unknown[]) { + throw new Error('not implemented'); +} +declare const nullableString: string | null; +assert(Boolean(nullableString)); + `, + }, + ], + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-condition.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-condition.shot index 4f7fef6518ec..824987f01dbd 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-condition.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-condition.shot @@ -15,6 +15,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing": { "description": "Whether to not error when running with a tsconfig that has strictNullChecks turned.", "type": "boolean" + }, + "checkTypePredicates": { + "description": "Whether to check the asserted argument of a type predicate function for unnecessary conditions", + "type": "boolean" } }, "type": "object" @@ -30,6 +34,8 @@ type Options = [ allowConstantLoopConditions?: boolean; /** Whether to not error when running with a tsconfig that has strictNullChecks turned. */ allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; + /** Whether to check the asserted argument of a type predicate function for unnecessary conditions */ + checkTypePredicates?: boolean; }, ]; " diff --git a/packages/type-utils/src/getTypeName.ts b/packages/type-utils/src/getTypeName.ts index ea3f69e4b5d6..72395f996ca0 100644 --- a/packages/type-utils/src/getTypeName.ts +++ b/packages/type-utils/src/getTypeName.ts @@ -23,8 +23,9 @@ export function getTypeName( // via AST. const symbol = type.getSymbol(); const decls = symbol?.getDeclarations(); - const typeParamDecl = decls?.[0] as ts.TypeParameterDeclaration; + const typeParamDecl = decls?.[0]; if ( + typeParamDecl != null && ts.isTypeParameterDeclaration(typeParamDecl) && typeParamDecl.constraint != null ) { diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index c191fc65c356..a5378e52bc9a 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -172,7 +172,7 @@ describe('semanticInfo', () => { ).declarations[0].init!; const tsBinaryExpression = parseResult.services.esTreeNodeToTSNodeMap.get(binaryExpression); - expectToBeDefined(tsBinaryExpression); + expect(tsBinaryExpression).toBeDefined(); expect(tsBinaryExpression.kind).toEqual(ts.SyntaxKind.BinaryExpression); const computedPropertyString = ( @@ -181,7 +181,7 @@ describe('semanticInfo', () => { ).key; const tsComputedPropertyString = parseResult.services.esTreeNodeToTSNodeMap.get(computedPropertyString); - expectToBeDefined(tsComputedPropertyString); + expect(tsComputedPropertyString).toBeDefined(); expect(tsComputedPropertyString.kind).toEqual(ts.SyntaxKind.StringLiteral); }); @@ -210,7 +210,7 @@ describe('semanticInfo', () => { expectToHaveParserServices(parseResult.services); const tsArrayBoundName = parseResult.services.esTreeNodeToTSNodeMap.get(arrayBoundName); - expectToBeDefined(tsArrayBoundName); + expect(tsArrayBoundName).toBeDefined(); checkNumberArrayType(checker, tsArrayBoundName); expect( @@ -235,7 +235,6 @@ describe('semanticInfo', () => { const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap.get(boundName); - expectToBeDefined(tsBoundName); expect(tsBoundName).toBeDefined(); expect(parseResult.services.tsNodeToESTreeNodeMap.get(tsBoundName)).toBe( @@ -420,7 +419,7 @@ function testIsolatedFile( // get type checker expectToHaveParserServices(parseResult.services); const checker = parseResult.services.program.getTypeChecker(); - expectToBeDefined(checker); + expect(checker).toBeDefined(); // get number node (ast shape validated by snapshot) const declaration = (parseResult.ast.body[0] as TSESTree.VariableDeclaration) @@ -431,7 +430,7 @@ function testIsolatedFile( // get corresponding TS node const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap.get(arrayMember); - expectToBeDefined(tsArrayMember); + expect(tsArrayMember).toBeDefined(); expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral); expect((tsArrayMember as ts.NumericLiteral).text).toBe('3'); @@ -451,7 +450,7 @@ function testIsolatedFile( const boundName = declaration.id as TSESTree.Identifier; expect(boundName.name).toBe('x'); const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap.get(boundName); - expectToBeDefined(tsBoundName); + expect(tsBoundName).toBeDefined(); checkNumberArrayType(checker, tsBoundName); expect(parseResult.services.tsNodeToESTreeNodeMap.get(tsBoundName)).toBe( boundName, diff --git a/packages/typescript-estree/tests/lib/source-files.test.ts b/packages/typescript-estree/tests/lib/source-files.test.ts index e6edb1c9c657..1986b25094e3 100644 --- a/packages/typescript-estree/tests/lib/source-files.test.ts +++ b/packages/typescript-estree/tests/lib/source-files.test.ts @@ -13,6 +13,7 @@ describe('isSourceFile', () => { it('returns true when given a real source file', () => { const input = ts.createSourceFile('test.ts', '', ts.ScriptTarget.ESNext); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentionally testing that the types match reality expect(isSourceFile(input)).toBe(true); }); }); diff --git a/packages/typescript-estree/tests/test-utils/test-utils.ts b/packages/typescript-estree/tests/test-utils/test-utils.ts index ed18eb6fb3fe..b307e755e880 100644 --- a/packages/typescript-estree/tests/test-utils/test-utils.ts +++ b/packages/typescript-estree/tests/test-utils/test-utils.ts @@ -83,7 +83,7 @@ export function deeplyCopy>(ast: T): T { type UnknownObject = Record; -function isObjectLike(value: unknown): value is UnknownObject { +function isObjectLike(value: unknown): boolean { return ( typeof value === 'object' && !(value instanceof RegExp) && value != null ); diff --git a/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx b/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx index c5dc1b37926d..745fd08992fa 100644 --- a/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx +++ b/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx @@ -6,7 +6,7 @@ import styles from '../ast/ASTViewer.module.css'; import PropertyName from '../ast/PropertyName'; import { tsEnumToString } from '../ast/tsUtils'; import type { OnHoverNodeFn } from '../ast/types'; -import { getRange, isTSNode } from '../ast/utils'; +import { getRange } from '../ast/utils'; export interface SimplifiedTreeViewProps { readonly value: ts.Node; @@ -31,7 +31,7 @@ function SimplifiedItem({ const onHover = useCallback( (v: boolean) => { - if (isTSNode(value) && onHoverNode) { + if (onHoverNode) { return onHoverNode(v ? getRange(value, 'tsNode') : undefined); } }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy