eslint-plugin-svelte
Version:
ESLint plugin for Svelte using AST
589 lines (588 loc) • 27 kB
JavaScript
import { createRule } from '../../utils/index.js';
import { isFalsyType, getConstrainedTypeAtLocation, isTruthyLiteral, isPossiblyFalsyType, isNullishType, isBooleanLiteralType, getTypeScriptTools, isAnyType, isUnknownType, isNeverType, getCallSignaturesOfType, isNullableType, getTypeOfPropertyOfType, getTypeName, isTupleType } from '../../utils/ts-utils/index.js';
import { getSourceCode } from '../../utils/compat.js';
/**
* Returns all types of a union type or an array containing `type` itself if it's no union type.
* This method is heavily inspired by tsutils. https://github.com/ajafff/tsutils
* The MIT License (MIT) Copyright (c) 2017 Klaus Meinhardt
* https://github.com/ajafff/tsutils/blob/master/LICENSE
*/
function unionTypeParts(type) {
return [...iterate(type)];
/**
* iterate
*/
function* iterate(t) {
if (t.isUnion()) {
for (const type of t.types) {
yield* iterate(type);
}
}
else {
yield t;
}
}
}
/**
* Check whether the given type can be a falsy type or not.
*/
function isPossiblyFalsy(type, tsTools) {
return (unionTypeParts(type)
// PossiblyFalsy flag includes literal values, so exclude ones that
// are definitely truthy
.filter((t) => !isTruthyLiteral(t, tsTools))
.some((type) => isPossiblyFalsyType(type, tsTools.ts)));
}
/**
* Check whether the given type can be a truthy type or not.
*/
function isPossiblyTruthy(type, tsTools) {
return unionTypeParts(type).some((type) => !isFalsyType(type, tsTools));
}
/**
* Check whether the given type can be a nullish type or not.
*/
function isPossiblyNullish(type, tsTools) {
return isNullableType(type, tsTools.ts);
}
/**
* Check whether the given type is a nullish type or not.
*/
function isAlwaysNullish(type, tsTools) {
return isNullishType(type, tsTools.ts);
}
/**
* Check whether the given type is a literal type or not.
*/
function isLiteral(type, tsTools) {
return (isBooleanLiteralType(type, tsTools.ts) || isNullishType(type, tsTools.ts) || type.isLiteral());
}
export default createRule('@typescript-eslint/no-unnecessary-condition', {
meta: {
docs: {
description: 'disallow conditionals where the type is always truthy or always falsy',
category: 'Extension Rules',
recommended: false,
extensionRule: {
plugin: '@typescript-eslint/eslint-plugin',
url: 'https://typescript-eslint.io/rules/no-unnecessary-condition/'
}
},
schema: [
{
type: 'object',
properties: {
allowConstantLoopConditions: {
description: 'Whether to ignore constant loop conditions, such as `while (true)`.',
type: 'boolean'
},
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
description: 'Whether to not error when running with a tsconfig that has strictNullChecks turned.',
type: 'boolean'
}
},
additionalProperties: false
}
],
fixable: 'code',
messages: {
alwaysTruthy: 'Unnecessary conditional, value is always truthy.',
alwaysFalsy: 'Unnecessary conditional, value is always falsy.',
alwaysTruthyFunc: 'This callback should return a conditional, but return is always truthy.',
alwaysFalsyFunc: 'This callback should return a conditional, but return is always falsy.',
neverNullish: 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.',
alwaysNullish: 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.',
literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values.',
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.'
},
type: 'suggestion',
deprecated: true,
replacedBy: {
note: 'This rule is no longer needed when using svelte-eslint-parser>=v0.19.0.'
}
},
create(context) {
const { allowConstantLoopConditions = false, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing = false } = (context.options[0] || {});
const tools = getTypeScriptTools(context);
if (!tools) {
return {};
}
const { service, ts } = tools;
const checker = service.program.getTypeChecker();
const sourceCode = getSourceCode(context);
const compilerOptions = service.program.getCompilerOptions();
const isStrictNullChecks = compilerOptions.strict
? compilerOptions.strictNullChecks !== false
: compilerOptions.strictNullChecks;
if (!isStrictNullChecks && allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true) {
context.report({
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 }
},
messageId: 'noStrictNullCheck'
});
}
const mutableVarReferenceIds = [];
const scriptElements = [];
let inSvelteReactiveStatement = false;
// Extract references to mutable variables in the root scope.
for (const scope of [
sourceCode.scopeManager.globalScope,
sourceCode.scopeManager.globalScope?.childScopes.find((scope) => scope.type === 'module')
]) {
if (!scope)
continue;
for (const variable of scope.variables) {
if (variable.defs.some((def) => def.type === 'Variable' && (def.parent.kind === 'var' || def.parent.kind === 'let'))) {
for (const reference of variable.references) {
mutableVarReferenceIds.push(reference.identifier);
}
}
}
}
// Extract <script> ranges.
for (const body of sourceCode.ast.body) {
if (body.type === 'SvelteScriptElement') {
scriptElements.push(body);
}
}
/**
* Checks whether the given expression node is in Svelte reactive scope
* and the variables that make up the given expression node use
* mutable variables declared in component root scope.
*/
function hasSvelteReactiveVar(node) {
const inReactiveScope = inSvelteReactiveStatement ||
(scriptElements.length &&
scriptElements.every((elem) => node.range[1] <= elem.range[0] || elem.range[1] <= node.range[0]));
if (!inReactiveScope) {
// The given expression node is neither in a reactive scope nor in a template scope.
return false;
}
return mutableVarReferenceIds.some((id) => node.range[0] <= id.range[0] && id.range[1] <= node.range[1]);
}
/** Get the TS type from ES Node */
function getNodeType(node) {
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
return tsNode && getConstrainedTypeAtLocation(checker, tsNode);
}
/**
* Check whether the given node is an array type or not.
*/
function nodeIsArrayType(node) {
const nodeType = getNodeType(node);
if (!nodeType) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- isArrayType is an internal API of TS.
return checker.isArrayType(nodeType);
}
/**
* Check whether the given node is an tuple type or not.
*/
function nodeIsTupleType(node) {
const nodeType = getNodeType(node);
return Boolean(nodeType && isTupleType(nodeType, ts));
}
/**
* Check whether the given node is an array index signature or not.
*/
function isArrayIndexExpression(node) {
return (
// Is an index signature
node.type === 'MemberExpression' &&
node.computed &&
// ...into an array type
(nodeIsArrayType(node.object) ||
// ... or a tuple type
(nodeIsTupleType(node.object) &&
// Exception: literal index into a tuple - will have a sound type
node.property.type !== 'Literal')));
}
/**
* Checks if a conditional node is necessary:
* if the type of the node is always true or always false, it's not necessary.
*/
function checkNode(node, isUnaryNotArgument = false) {
if (hasSvelteReactiveVar(node)) {
return;
}
// Check if the node is Unary Negation expression and handle it
if (node.type === 'UnaryExpression' && node.operator === '!') {
checkNode(node.argument, true);
return;
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (isArrayIndexExpression(node)) {
return;
}
// When checking logical expressions, only check the right side
// as the left side has been checked by checkLogicalExpressionForUnnecessaryConditionals
//
// Unless the node is nullish coalescing, as it's common to use patterns like `nullBool ?? true` to to strict
// boolean checks if we inspect the right here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
if (node.type === 'LogicalExpression' && node.operator !== '??') {
checkNode(node.right);
return;
}
const type = getNodeType(node);
// Conditional is always necessary if it involves:
// `any` or `unknown` or a naked type parameter
if (!type ||
unionTypeParts(type).some((part) => isAnyType(part, ts) || isUnknownType(part, ts) || part.isTypeParameter())) {
return;
}
let messageId = null;
if (unionTypeParts(type).some((part) => isNeverType(part, ts))) {
messageId = 'never';
}
else if (!isPossiblyTruthy(type, tools)) {
messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
}
else if (!isPossiblyFalsy(type, tools)) {
messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
}
if (messageId) {
context.report({ node, messageId });
}
}
/**
* Checks if a conditional node is necessary from the given lhs of nullish coalescing.
*/
function checkNodeForNullish(node) {
if (hasSvelteReactiveVar(node)) {
return;
}
const type = getNodeType(node);
// Conditional is always necessary if it involves `any` or `unknown`
if (!type || isAnyType(type, ts) || isUnknownType(type, ts)) {
return;
}
let messageId = null;
if (unionTypeParts(type).some((part) => isNeverType(part, ts))) {
messageId = 'never';
}
else if (!isPossiblyNullish(type, tools)) {
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (!isArrayIndexExpression(node) &&
!(node.type === 'ChainExpression' &&
node.expression.type !== 'TSNonNullExpression' &&
optionChainContainsOptionArrayIndex(node.expression))) {
messageId = 'neverNullish';
}
}
else if (isAlwaysNullish(type, tools)) {
messageId = 'alwaysNullish';
}
if (messageId) {
context.report({ node, messageId });
}
}
/**
* Checks that a binary expression is necessarily conditional, reports otherwise.
* If both sides of the binary expression are literal values, it's not a necessary condition.
*
* NOTE: It's also unnecessary if the types that don't overlap at all
* but that case is handled by the Typescript compiler itself.
* Known exceptions:
* * https://github.com/microsoft/TypeScript/issues/32627
* * https://github.com/microsoft/TypeScript/issues/37160 (handled)
*/
const BOOL_OPERATORS = new Set(['<', '>', '<=', '>=', '==', '===', '!=', '!==']);
/**
* Checks if a conditional node is necessary from the given binary expression.
*/
function checkIfBinaryExpressionIsNecessaryConditional(node) {
if (hasSvelteReactiveVar(node)) {
return;
}
if (!BOOL_OPERATORS.has(node.operator)) {
return;
}
const leftType = getNodeType(node.left);
const rightType = getNodeType(node.right);
if (!leftType || !rightType) {
return;
}
if (isLiteral(leftType, tools) && isLiteral(rightType, tools)) {
context.report({ node, messageId: 'literalBooleanExpression' });
return;
}
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
if (isStrictNullChecks) {
const UNDEFINED = ts.TypeFlags.Undefined;
const NULL = ts.TypeFlags.Null;
// eslint-disable-next-line func-style -- ignore
const isComparable = (type, f) => {
let flag = f;
// Allow comparison to `any`, `unknown` or a naked type parameter.
flag |= ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter;
// Allow loose comparison to nullish values.
if (node.operator === '==' || node.operator === '!=') {
flag |= NULL | UNDEFINED;
}
return unionTypeParts(type).some((t) => (t.flags & flag) !== 0);
};
if ((leftType.flags === UNDEFINED && !isComparable(rightType, UNDEFINED)) ||
(rightType.flags === UNDEFINED && !isComparable(leftType, UNDEFINED)) ||
(leftType.flags === NULL && !isComparable(rightType, NULL)) ||
(rightType.flags === NULL && !isComparable(leftType, NULL))) {
context.report({ node, messageId: 'noOverlapBooleanExpression' });
}
}
}
/**
* Checks that a logical expression contains a boolean, reports otherwise.
*/
function checkLogicalExpressionForUnnecessaryConditionals(node) {
if (node.operator === '??') {
checkNodeForNullish(node.left);
return;
}
// Only checks the left side, since the right side might not be "conditional" at all.
// The right side will be checked if the LogicalExpression is used in a conditional context
checkNode(node.left);
}
/**
* Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
*/
function checkIfLoopIsNecessaryConditional(node) {
if (node.test === null) {
// e.g. `for(;;)`
return;
}
/**
* Allow:
* while (true) {}
* for (;true;) {}
* do {} while (true)
*/
if (allowConstantLoopConditions) {
const nodeType = getNodeType(node.test);
if (nodeType &&
isBooleanLiteralType(nodeType, ts) &&
checker.typeToString(nodeType) === 'true')
return;
}
checkNode(node.test);
}
const ARRAY_PREDICATE_FUNCTIONS = new Set(['filter', 'find', 'some', 'every']);
/**
* Checks whether the given call expression is an array method that takes a predicate as an argument.
*/
function isArrayPredicateFunction(node) {
const { callee } = node;
return (
// looks like `something.filter` or `something.find`
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
ARRAY_PREDICATE_FUNCTIONS.has(callee.property.name) &&
// and the left-hand side is an array, according to the types
(nodeIsArrayType(callee.object) || nodeIsTupleType(callee.object)));
}
/**
* Checks if a conditional node is necessary from the given call expression.
*/
function checkCallExpression(node) {
// If this is something like arr.filter(x => /*condition*/), check `condition`
if (isArrayPredicateFunction(node) && node.arguments.length) {
const callback = node.arguments[0];
// Inline defined functions
if ((callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression') &&
callback.body) {
// Two special cases, where we can directly check the node that's returned:
// () => something
if (callback.body.type !== 'BlockStatement') {
checkNode(callback.body);
return;
}
// () => { return something; }
const callbackBody = callback.body.body;
if (callbackBody.length === 1 &&
callbackBody[0].type === 'ReturnStatement' &&
callbackBody[0].argument) {
checkNode(callbackBody[0].argument);
return;
}
// Potential enhancement: could use code-path analysis to check
// any function with a single return statement
// (Value to complexity ratio is dubious however)
}
const nodeType = getNodeType(callback);
if (!nodeType) {
return;
}
// Otherwise just do type analysis on the function as a whole.
const returnTypes = getCallSignaturesOfType(nodeType).map((sig) => sig.getReturnType());
/* istanbul ignore if */ if (returnTypes.length === 0) {
// Not a callable function
return;
}
// Predicate is always necessary if it involves `any` or `unknown`
if (returnTypes.some((t) => isAnyType(t, ts) || isUnknownType(t, ts))) {
return;
}
if (!returnTypes.some((t) => isPossiblyFalsy(t, tools))) {
context.report({
node: callback,
messageId: 'alwaysTruthyFunc'
});
return;
}
if (!returnTypes.some((t) => isPossiblyTruthy(t, tools))) {
context.report({
node: callback,
messageId: 'alwaysFalsyFunc'
});
}
}
}
/**
*
* Recursively searches an optional chain for an array index expression
* Has to search the entire chain, because an array index will "infect" the rest of the types
* Example:
* ```
* [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
* ?.x // type is {y: "z"}
* ?.y // This access is considered "unnecessary" according to the types
* ```
*/
function optionChainContainsOptionArrayIndex(node) {
const lhsNode = node.type === 'CallExpression' ? node.callee : node.object;
if (node.optional && isArrayIndexExpression(lhsNode)) {
return true;
}
if (lhsNode.type === 'MemberExpression' || lhsNode.type === 'CallExpression') {
return optionChainContainsOptionArrayIndex(lhsNode);
}
return false;
}
/**
* Check whether the given property is a nullable or not.
*/
function isNullablePropertyType(objType, propertyType) {
if (propertyType.isUnion()) {
return propertyType.types.some((type) => isNullablePropertyType(objType, type));
}
if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
const propType = getTypeOfPropertyOfType(objType, propertyType.value.toString(), checker);
if (propType) {
return isNullableType(propType, ts);
}
}
const typeName = getTypeName(propertyType, tools);
return Boolean((typeName === 'string' && checker.getIndexInfoOfType(objType, ts.IndexKind.String)) ||
(typeName === 'number' && checker.getIndexInfoOfType(objType, ts.IndexKind.Number)));
}
/**
* Checks whether a member expression is nullable or not regardless of it's previous node.
* Example:
* ```
* // 'bar' is nullable if 'foo' is null.
* // but this function checks regardless of 'foo' type, so returns 'true'.
* declare const foo: { bar : { baz: string } } | null
* foo?.bar;
* ```
*/
function isNullableOriginFromPrev(node) {
const prevType = getNodeType(node.object);
const property = node.property;
if (prevType && prevType.isUnion() && property.type === 'Identifier') {
const isOwnNullable = prevType.types.some((type) => {
if (node.computed) {
const propertyType = getNodeType(node.property);
return Boolean(propertyType && isNullablePropertyType(type, propertyType));
}
const propType = getTypeOfPropertyOfType(type, property.name, checker);
return propType && isNullableType(propType, ts);
});
return !isOwnNullable && isNullableType(prevType, ts);
}
return false;
}
/**
* Checks whether a lhs expression is optionable or not.
*/
function isOptionableExpression(node) {
const type = getNodeType(node);
if (!type) {
return false;
}
const isOwnNullable = node.type === 'MemberExpression' ? !isNullableOriginFromPrev(node) : true;
return (isAnyType(type, ts) ||
isUnknownType(type, ts) ||
(isNullableType(type, ts) && isOwnNullable));
}
/**
* Checks if a conditional node is necessary from the given optional chaining expression.
*/
function checkOptionalChain(node, beforeOperator, fix) {
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
if (!node.optional) {
return;
}
// Since typescript array index signature types don't represent the
// possibility of out-of-bounds access, if we're indexing into an array
// just skip the check, to avoid false positives
if (optionChainContainsOptionArrayIndex(node)) {
return;
}
const nodeToCheck = node.type === 'CallExpression' ? node.callee : node.object;
if (hasSvelteReactiveVar(nodeToCheck)) {
return;
}
if (isOptionableExpression(nodeToCheck)) {
return;
}
const questionDotOperator = sourceCode.getTokenAfter(beforeOperator, {
includeComments: false,
filter: (token) => token.type === 'Punctuator' && token.value === '?.'
});
context.report({
node,
loc: questionDotOperator.loc,
messageId: 'neverOptionalChain',
fix(fixer) {
return fixer.replaceText(questionDotOperator, fix);
}
});
}
/**
* Checks for optional member expression
*/
function checkOptionalMemberExpression(node) {
checkOptionalChain(node, node.object, node.computed ? '' : '.');
}
/**
* Checks for optional call expression
*/
function checkOptionalCallExpression(node) {
checkOptionalChain(node, node.callee, '');
}
return {
SvelteReactiveStatement: () => (inSvelteReactiveStatement = true),
'SvelteReactiveStatement:exit': () => (inSvelteReactiveStatement = false),
BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional,
CallExpression: checkCallExpression,
ConditionalExpression: (node) => checkNode(node.test),
DoWhileStatement: checkIfLoopIsNecessaryConditional,
ForStatement: checkIfLoopIsNecessaryConditional,
IfStatement: (node) => checkNode(node.test),
LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
WhileStatement: checkIfLoopIsNecessaryConditional,
'MemberExpression[optional = true]': checkOptionalMemberExpression,
'CallExpression[optional = true]': checkOptionalCallExpression
};
}
});