UNPKG

eslint-plugin-sonarjs

Version:
227 lines (226 loc) • 9.65 kB
"use strict"; /* * SonarQube JavaScript Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * You can redistribute and/or modify this program under the terms of * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the Sonar Source-Available License for more details. * * You should have received a copy of the Sonar Source-Available License * along with this program; if not, see https://sonarsource.com/license/ssal/ */ // https://sonarsource.github.io/rspec/#/rspec/S9073/javascript var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const ancestor_js_1 = require("../helpers/ancestor.js"); const ast_js_1 = require("../helpers/ast.js"); const assertions_js_1 = require("../helpers/assertions.js"); const equivalence_js_1 = require("../helpers/equivalence.js"); const generate_meta_js_1 = require("../helpers/generate-meta.js"); const meta = __importStar(require("./generated-meta.js")); const messages = { issue: 'This composite assertion hides which condition failed; split it to make failures actionable.', }; exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(meta, { messages }), create(context) { return { CallExpression(node) { const assertion = (0, assertions_js_1.extractTestAssertion)(context, node); if (assertion?.kind !== 'predicate' || (assertion.style !== 'node-assert' && assertion.style !== 'jest-like') || (assertion.predicate !== 'truthy' && assertion.predicate !== 'falsy')) { return; } const isTruthy = (assertion.predicate === 'truthy') !== assertion.negated; const actual = assertion.actual; const truthyConjunction = getTruthyConjunction(actual, isTruthy); const isComposite = (isTruthy && ((actual.type === 'LogicalExpression' && actual.operator === '&&') || (actual.type === 'UnaryExpression' && actual.operator === '!' && actual.argument.type === 'LogicalExpression' && actual.argument.operator === '||'))) || (!isTruthy && ((actual.type === 'LogicalExpression' && actual.operator === '||') || (actual.type === 'UnaryExpression' && actual.operator === '!' && actual.argument.type === 'LogicalExpression' && actual.argument.operator === '&&'))); if (isComposite && !(truthyConjunction && isGuardChain(truthyConjunction, context.sourceCode))) { context.report({ node: actual, messageId: 'issue' }); } }, }; }, }; function getTruthyConjunction(actual, isTruthy) { if (isTruthy && actual.type === 'LogicalExpression' && actual.operator === '&&') { return actual; } if (!isTruthy && actual.type === 'UnaryExpression' && actual.operator === '!' && actual.argument.type === 'LogicalExpression' && actual.argument.operator === '&&') { return actual.argument; } return undefined; } /** * A conjunction is one cohesive check rather than independent conditions when an operand checks * the existence, type, or shape of a reference and every operand only uses that same reference. */ function isGuardChain(expression, sourceCode) { const operands = flattenConjunction(expression); return operands .flatMap(getCheckedSubjects) .some(subject => operands.every(operand => mentions(operand, subject, sourceCode))); } function flattenConjunction(expression) { if (expression.type === 'LogicalExpression' && expression.operator === '&&') { return [...flattenConjunction(expression.left), ...flattenConjunction(expression.right)]; } return [expression]; } /** * Returns the references whose existence, type, or shape the operand checks. Comparing a property * checks that property, not the object holding it, so `result.kind === 'success'` checks nothing * about `result`. Likewise, an arbitrary call such as `compute(value)` checks nothing about * `value` unless its name signals that it is a predicate, e.g. `isNonEmpty(value)`. */ function getCheckedSubjects(operand) { if (isStableReference(operand)) { return [operand]; } if (operand.type === 'CallExpression' && isPredicateCall(operand)) { return isStableReference(operand.arguments[0]) ? [operand.arguments[0]] : []; } return operand.type === 'BinaryExpression' ? getComparisonSubjects(operand) : []; } function isPredicateCall(expression) { if (expression.arguments.length !== 1 || expression.arguments[0].type === 'SpreadElement') { return false; } const { callee } = expression; return ((callee.type === 'Identifier' && /^is[A-Z_]/u.test(callee.name)) || (callee.type === 'MemberExpression' && !callee.computed && callee.object.type === 'Identifier' && callee.object.name === 'Array' && callee.property.type === 'Identifier' && callee.property.name === 'isArray')); } function getComparisonSubjects({ left, operator, right, }) { switch (operator) { case 'instanceof': return isStableReference(left) ? [left] : []; case 'in': return isStringLiteral(left) && isStableReference(right) ? [right] : []; case '!=': case '!==': return getDefinedSubjects(left, right); case '==': case '===': return getTypeofSubjects(left, right); default: return []; } } function getDefinedSubjects(left, right) { if (isAbsentValue(left) && isStableReference(right)) { return [right]; } if (isAbsentValue(right) && isStableReference(left)) { return [left]; } return []; } function getTypeofSubjects(left, right) { const subject = getTypeofSubject(left, right) ?? getTypeofSubject(right, left); return subject ? [subject] : []; } function getTypeofSubject(candidateTypeof, candidateLiteral) { return candidateTypeof.type === 'UnaryExpression' && candidateTypeof.operator === 'typeof' && isStringLiteral(candidateLiteral) && isStableReference(candidateTypeof.argument) ? candidateTypeof.argument : undefined; } function isStringLiteral(node) { return node.type === 'Literal' && typeof node.value === 'string'; } function isAbsentValue(node) { return (0, ast_js_1.isNullLiteral)(node) || (0, ast_js_1.isUndefined)(node); } /** * Whether the expression uses the subject. Nested functions are skipped because they can shadow * the reference, in which case a match would denote a different value. */ function mentions(node, subject, sourceCode) { if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression' || node.type === 'ClassExpression') { return false; } if ((0, equivalence_js_1.areEquivalent)(node, subject, sourceCode)) { return true; } // A non-computed property name is not a reference, so `b.a` does not use `a`. const children = node.type === 'MemberExpression' && !node.computed ? [node.object] : (0, ancestor_js_1.childrenOf)(node, sourceCode.visitorKeys); return children.some(child => mentions(child, subject, sourceCode)); } function isStableReference(node) { if (node.type === 'Identifier' || node.type === 'ThisExpression') { return true; } if (node.type !== 'MemberExpression' || !isStableReference(node.object)) { return false; } return ((!node.computed && (node.property.type === 'Identifier' || node.property.type === 'PrivateIdentifier')) || (node.computed && node.property.type === 'Literal')); }