UNPKG

@dfinity/eslint-config-oisy-wallet

Version:

Shared ESLint configurations from the Oisy Wallet team

369 lines (300 loc) 9.69 kB
const ts = require("typescript"); const NULLISH_UTILS = new Set(["isNullish", "nonNullish"]); // TODO: shall we expand it with `==` and `!=` ? const EQ_OPS = new Set(["==="]); const NOT_EQ_OPS = new Set(["!=="]); const COMPARISON_OPS = new Set([...EQ_OPS, ...NOT_EQ_OPS]); const BOOLEAN_BINARY_OPS = new Set([...COMPARISON_OPS, ">", "<", ">=", "<="]); const KNOWN_BOOLEAN_METHODS = new Set([ "includes", "startsWith", "endsWith", "test", "some", "every", ]); module.exports = { meta: { type: "suggestion", docs: { description: "Enforce the use of isNullish functions for nullish checks", category: "Best Practices", recommended: false, }, messages: { isNullish: "Use isNullish() instead of direct variable check for nullish checks.", nonNullish: "Use nonNullish() instead of direct variable check for nullish checks.", }, fixable: "code", schema: [ { type: "object", properties: { includeBooleans: { type: "boolean", description: "When true, also enforce nullish checks on variables typed as boolean or nullish boolean. " + "Boolean expressions (comparisons, known boolean methods, literals, negations) are always allowed. " + "Defaults to false.", }, allowFindUndefinedCheck: { type: "boolean", description: "When true, allow direct nullish comparisons on the result of .find() calls " + "(e.g. array.find(x => x) === undefined). " + "Defaults to false.", }, }, additionalProperties: false, }, ], }, create: (context) => { const [options] = context.options; const shouldLintBooleans = options?.includeBooleans ?? false; const allowFindUndefinedCheck = options?.allowFindUndefinedCheck ?? false; const { sourceCode } = context; const parserServices = sourceCode?.parserServices ?? context.parserServices; const checker = parserServices?.program?.getTypeChecker(); const isNullishUtilityCall = (node) => node.type === "CallExpression" && node.callee.type === "Identifier" && NULLISH_UTILS.has(node.callee.name); const hasKnownBooleanMethodCall = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && KNOWN_BOOLEAN_METHODS.has(node.callee.property.name); const isBooleanBinaryExpression = (node) => node.type === "BinaryExpression" && BOOLEAN_BINARY_OPS.has(node.operator); const isBooleanTypeAnnotation = (typeAnn) => { if (!typeAnn) { return false; } if (typeAnn.type === "TSBooleanKeyword") { return true; } if (typeAnn.type !== "TSUnionType") { return false; } return typeAnn.types.every( (typeNode) => typeNode.type === "TSBooleanKeyword" || typeNode.type === "TSNullKeyword" || typeNode.type === "TSUndefinedKeyword" || (typeNode.type === "TSLiteralType" && typeof typeNode.literal?.value === "boolean"), ); }; const findVariableTypeAnnotation = ({ scope, variableName }) => { if (!scope) { return; } const variable = scope.set.get(variableName); const [def] = variable?.defs ?? []; const typeAnn = def?.node?.id?.typeAnnotation?.typeAnnotation ?? def?.node?.typeAnnotation?.typeAnnotation; return ( typeAnn ?? findVariableTypeAnnotation({ scope: scope.upper, variableName }) ); }; const isBooleanTypeFromScope = (node) => { if (node.type === "Literal" && typeof node.value === "boolean") { return true; } if (isBooleanBinaryExpression(node)) { return true; } if (node.type === "UnaryExpression" && node.operator === "!") { return true; } if (isNullishUtilityCall(node)) { return true; } if (hasKnownBooleanMethodCall(node)) { return true; } if (node.type !== "Identifier") { return false; } const initialScope = sourceCode.getScope(node); const typeAnn = findVariableTypeAnnotation({ scope: initialScope, variableName: node.name, }); return isBooleanTypeAnnotation(typeAnn); }; const isBooleanTypeFromTs = (node) => { if (!checker || !parserServices) { return false; } const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(tsNode); const isStrictBoolean = (typeToCheck) => (typeToCheck.getFlags() & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) !== 0; if (isStrictBoolean(type)) { return true; } return ( type.isUnion() && type.types.every( (typeToCheck) => (typeToCheck.getFlags() & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0, ) ); }; const isBooleanExpression = (node) => isBooleanBinaryExpression(node) || hasKnownBooleanMethodCall(node) || isNullishUtilityCall(node) || (node.type === "Literal" && typeof node.value === "boolean") || (node.type === "UnaryExpression" && node.operator === "!"); const shouldTreatAsBooleanCondition = (node) => { if (shouldLintBooleans) { return isBooleanExpression(node); } try { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return isBooleanTypeFromTs(node) || isBooleanTypeFromScope(node); } catch { return isBooleanTypeFromScope(node); } }; const isNullishLiteral = (expression) => (expression.type === "Identifier" && expression.name === "undefined") || (expression.type === "Literal" && expression.value === null); const isFindCall = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && node.callee.property.name === "find"; const getNullishComparisonTarget = (node) => { if (node.type !== "BinaryExpression") { return; } if (!COMPARISON_OPS.has(node.operator)) { return; } if (isNullishLiteral(node.right)) { return node.left; } if (isNullishLiteral(node.left)) { return node.right; } }; const getNullishReplacement = (operator) => EQ_OPS.has(operator) ? "isNullish" : "nonNullish"; const report = ({ node, messageId, replacementFn, fixNode }) => { context.report({ node, messageId, fix: (fixer) => fixer.replaceText( node, `${replacementFn}(${sourceCode.getText(fixNode)})`, ), }); }; const checkCondition = (node) => { if (!node) { return; } if (isNullishUtilityCall(node)) { return; } if (getNullishComparisonTarget(node)) { return; } if (node.type === "UnaryExpression" && node.operator === "!") { return; } if (node.type === "LogicalExpression") { checkCondition(node.left); checkCondition(node.right); return; } if (shouldTreatAsBooleanCondition(node)) { return; } report({ node, messageId: "nonNullish", replacementFn: "nonNullish", fixNode: node, }); }; // ESLint AST puts the condition of if/while/for/ternary in `node.test`, so we analyse that specifically const checkTestCondition = (node) => { if (node.test) { checkCondition(node.test); } }; return { BinaryExpression: (node) => { const target = getNullishComparisonTarget(node); if (!target) { return; } if (allowFindUndefinedCheck && isFindCall(target)) { return; } const replacementFn = getNullishReplacement(node.operator); report({ node, messageId: replacementFn, replacementFn, fixNode: target, }); }, UnaryExpression: (node) => { if ( node.operator === "!" && node.argument.type === "UnaryExpression" && node.argument.operator === "!" ) { if (shouldTreatAsBooleanCondition(node.argument.argument)) { return; } report({ node, messageId: "nonNullish", replacementFn: "nonNullish", fixNode: node.argument.argument, }); return; } if (node.operator === "!") { if (shouldTreatAsBooleanCondition(node.argument)) { return; } const { parent } = node; if (parent?.type === "UnaryExpression" && parent.operator === "!") { return; } report({ node, messageId: "isNullish", replacementFn: "isNullish", fixNode: node.argument, }); } }, IfStatement: checkTestCondition, WhileStatement: checkTestCondition, DoWhileStatement: checkTestCondition, ForStatement: checkTestCondition, ConditionalExpression: checkTestCondition, }; }, };