UNPKG

eslint-plugin-sonarjs

Version:
368 lines (367 loc) • 14.7 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/S9135/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 ast_js_1 = require("../helpers/ast.js"); const ancestor_js_1 = require("../helpers/ancestor.js"); const equivalence_js_1 = require("../helpers/equivalence.js"); const location_js_1 = require("../helpers/location.js"); const module_js_1 = require("../helpers/module.js"); const generate_meta_js_1 = require("../helpers/generate-meta.js"); const meta = __importStar(require("./generated-meta.js")); const LODASH_MESSAGE = 'Mutating a nested property of this shallow clone changes the original value; use structuredClone() or _.cloneDeep() when isolation is required.'; const UNDERSCORE_MESSAGE = 'Mutating a nested property of this shallow clone changes the original value; use structuredClone() when isolation is required.'; const SECONDARY_MESSAGE = 'Shallow clone created here.'; const NOSONAR_COMMENT = ' // NOSONAR: shared nested state is intentional'; const CLONE_LIBRARIES = new Map([ ['lodash.clone', 'lodash'], ['lodash-es.clone', 'lodash'], ['underscore.clone', 'underscore'], ]); const DEEP_CLONE_FQNS = new Set(['lodash.cloneDeep', 'lodash-es.cloneDeep']); const SHALLOW_CLONE_FQNS = new Set(['lodash.clone', 'lodash-es.clone', 'underscore.clone']); exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(meta, { hasSuggestions: true }), create(context) { return { AssignmentExpression: (node) => { const assignment = node; checkMutation(context, assignment.left, assignment); }, UpdateExpression: (node) => { const update = node; checkMutation(context, update.argument, update); }, UnaryExpression: (node) => { const unary = node; if (unary.operator === 'delete') { checkMutation(context, unary.argument, unary); } }, }; }, }; function checkMutation(context, mutatedNode, mutation) { const statement = getMutationStatement(context, mutation); if (statement === undefined) { return; } const memberChain = getStaticMemberChain(mutatedNode); if (memberChain === undefined) { return; } const cloneCall = getCloneCall(context, memberChain.root); if (cloneCall === undefined) { return; } const library = getCloneLibrary(context, cloneCall); if (library === undefined) { return; } const argument = cloneCall.arguments[0]; if (argument === undefined || argument.type === 'SpreadElement') { return; } if (hasIsolatingPrefixAssignment(context, mutatedNode, mutation)) { return; } const nosonarFix = getNosonarFix(context, statement, mutation); (0, location_js_1.report)(context, { node: mutation, message: library === 'underscore' ? UNDERSCORE_MESSAGE : LODASH_MESSAGE, suggest: [ { desc: 'Replace the shallow clone with structuredClone()', fix: replaceWithStructuredClone(cloneCall, argument, context), }, ...(nosonarFix === undefined ? [] : [ { desc: 'Add // NOSONAR: shared nested state is intentional', fix: nosonarFix, }, ]), ], }, [(0, location_js_1.toSecondaryLocation)(cloneCall, SECONDARY_MESSAGE)]); } function getMutationStatement(context, node) { let parent = (0, ancestor_js_1.getParent)(context, node); while (parent !== undefined) { if (parent.type.endsWith('Statement') || parent.type === 'VariableDeclaration') { return parent; } parent = (0, ancestor_js_1.getParent)(context, parent); } return undefined; } function getStaticMemberChain(node) { let current = (0, ast_js_1.unwrapTypeScriptExpression)(node); let depth = 0; while (current.type === 'MemberExpression') { if (!isStaticMember(current)) { return undefined; } depth += 1; current = (0, ast_js_1.unwrapTypeScriptExpression)(current.object); } return current.type === 'Identifier' && depth >= 2 ? { root: current, depth } : undefined; } function isStaticMember(member) { if (!member.computed) { return member.property.type === 'Identifier'; } return (member.property.type === 'Literal' && (typeof member.property.value === 'string' || typeof member.property.value === 'number')); } function getCloneCall(context, root) { const variable = (0, ast_js_1.getVariableFromName)(context, root.name, root); const writeExpression = (0, ast_js_1.getUniqueWriteReference)(variable); if (writeExpression?.type !== 'CallExpression' || writeExpression.arguments.length !== 1) { return undefined; } return writeExpression; } function getCloneLibrary(context, cloneCall) { return CLONE_LIBRARIES.get((0, module_js_1.getFullyQualifiedName)(context, cloneCall.callee) ?? ''); } function replaceWithStructuredClone(cloneCall, argument, context) { const argumentText = context.sourceCode.getText(argument); return (fixer) => fixer.replaceText(cloneCall, `structuredClone(${argumentText})`); } function getNosonarFix(context, statement, mutation) { if (statement.loc?.start.line !== mutation.loc?.start.line || statement.loc?.end.line !== mutation.loc?.start.line || !hasNoTokensAfter(context, statement)) { return undefined; } return (fixer) => fixer.insertTextAfter(statement, NOSONAR_COMMENT); } function hasNoTokensAfter(context, node) { const range = node.range; if (range === undefined) { return false; } const source = context.sourceCode.getText(); const lineEnd = source.indexOf('\n', range[1]); return source.slice(range[1], lineEnd === -1 ? source.length : lineEnd).trim() === ''; } function hasIsolatingPrefixAssignment(context, mutatedNode, mutation) { const target = (0, ast_js_1.unwrapTypeScriptExpression)(mutatedNode); if (target.type !== 'MemberExpression') { return false; } let extraDepth = 1; let prefix = (0, ast_js_1.unwrapTypeScriptExpression)(target.object); while (prefix.type === 'MemberExpression') { const isolation = findDominatingIsolation(context, prefix, mutation); if (isolation === 'deep' || (isolation === 'shallow' && extraDepth === 1)) { return true; } extraDepth += 1; prefix = (0, ast_js_1.unwrapTypeScriptExpression)(prefix.object); } return false; } function findDominatingIsolation(context, prefix, mutation) { let current = mutation; let latest; while (current !== undefined) { const parent = (0, ancestor_js_1.getParent)(context, current); if (parent === undefined || ast_js_1.FUNCTION_NODES.includes(parent.type)) { break; } const candidate = getPriorIsolation(context, parent, current, prefix); if (latest === undefined && candidate !== undefined) { latest = candidate; } if (latest === 'deep') { return 'deep'; } current = parent; } return latest === null ? undefined : latest; } function getPriorIsolation(context, parent, current, prefix) { if (parent.type === 'SequenceExpression') { const index = parent.expressions.indexOf(current); return index > 0 ? isolationInNodes(parent.expressions.slice(0, index), prefix, context) : undefined; } const statements = getBlockStatements(parent); if (statements === undefined) { return undefined; } const index = statements.indexOf(current); return index > 0 ? isolationInStatements(statements.slice(0, index), prefix, context) : undefined; } function getBlockStatements(node) { if (node.type === 'BlockStatement' || node.type === 'Program') { return node.body; } if (node.type === 'SwitchCase') { return node.consequent; } return undefined; } function isolationInStatements(statements, prefix, context) { let latest; for (const statement of statements) { const candidate = statementAssignsPrefix(statement, prefix, context); if (candidate !== undefined) { latest = candidate; } } return latest; } function isolationInNodes(nodes, prefix, context) { let latest; for (const node of nodes) { const candidate = expressionAssignsPrefix(node, prefix, context); if (candidate !== undefined) { latest = candidate; } } return latest; } function statementAssignsPrefix(statement, prefix, context) { if (statement.type === 'ExpressionStatement') { return expressionAssignsPrefix(statement.expression, prefix, context); } if (statement.type === 'BlockStatement') { return isolationInStatements(statement.body, prefix, context); } if (statement.type === 'LabeledStatement') { return statementAssignsPrefix(statement.body, prefix, context); } return containsPrefixWrite(statement, prefix, context) ? null : undefined; } function expressionAssignsPrefix(expression, prefix, context) { const value = (0, ast_js_1.unwrapTypeScriptExpression)(expression); if (value.type === 'SequenceExpression') { return isolationInNodes(value.expressions, prefix, context); } if (value.type === 'AssignmentExpression') { if (isSameBoundMember(value.left, prefix, context)) { if (value.operator !== '=') { return null; } return getFreshObjectIsolation(context, value.right) ?? null; } return containsPrefixWrite(value.right, prefix, context) ? null : undefined; } return containsPrefixWrite(value, prefix, context) ? null : undefined; } function containsPrefixWrite(node, prefix, context) { const current = (0, ast_js_1.unwrapTypeScriptExpression)(node); if (ast_js_1.FUNCTION_NODES.includes(current.type)) { return false; } if (isPrefixWrite(current, prefix, context)) { return true; } return (0, ancestor_js_1.childrenOf)(current, context.sourceCode.visitorKeys).some((child) => containsPrefixWrite(child, prefix, context)); } function isPrefixWrite(node, prefix, context) { if (node.type === 'AssignmentExpression') { return isSameBoundMember(node.left, prefix, context); } if (node.type === 'UpdateExpression') { return isSameBoundMember(node.argument, prefix, context); } return (node.type === 'UnaryExpression' && node.operator === 'delete' && isSameBoundMember(node.argument, prefix, context)); } function isSameBoundMember(assignmentLeft, prefix, context) { const left = (0, ast_js_1.unwrapTypeScriptExpression)(assignmentLeft); const target = (0, ast_js_1.unwrapTypeScriptExpression)(prefix); if (!(0, equivalence_js_1.areEquivalent)(left, target, context.sourceCode)) { return false; } const leftRoot = getStaticRootIdentifier(left); const prefixRoot = getStaticRootIdentifier(target); if (leftRoot === undefined || prefixRoot === undefined) { return false; } const leftVariable = (0, ast_js_1.getVariableFromName)(context, leftRoot.name, leftRoot); const prefixVariable = (0, ast_js_1.getVariableFromName)(context, prefixRoot.name, prefixRoot); return leftVariable !== undefined && leftVariable === prefixVariable; } function getStaticRootIdentifier(node) { let current = (0, ast_js_1.unwrapTypeScriptExpression)(node); while (current.type === 'MemberExpression') { current = (0, ast_js_1.unwrapTypeScriptExpression)(current.object); } return current.type === 'Identifier' ? current : undefined; } function getFreshObjectIsolation(context, rhs) { const value = (0, ast_js_1.unwrapTypeScriptExpression)(rhs); if (value.type === 'ObjectExpression' || value.type === 'ArrayExpression') { return 'shallow'; } if (value.type !== 'CallExpression') { return undefined; } if ((0, ast_js_1.isIdentifier)(value.callee, 'structuredClone')) { return 'deep'; } const fqn = (0, module_js_1.getFullyQualifiedName)(context, value.callee) ?? ''; if (DEEP_CLONE_FQNS.has(fqn)) { return 'deep'; } if (SHALLOW_CLONE_FQNS.has(fqn)) { return 'shallow'; } return undefined; }