UNPKG

eslint-plugin-sonarjs

Version:
541 lines (540 loc) 20.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.functionLike = exports.FUNCTION_NODES = void 0; exports.isModuleDeclaration = isModuleDeclaration; exports.isFunctionExpression = isFunctionExpression; exports.isFunctionDeclaration = isFunctionDeclaration; exports.isArrowFunctionExpression = isArrowFunctionExpression; exports.isIdentifier = isIdentifier; exports.getProgramStatements = getProgramStatements; exports.isIfStatement = isIfStatement; exports.isMemberWithProperty = isMemberWithProperty; exports.isThrowStatement = isThrowStatement; exports.isMemberExpression = isMemberExpression; exports.isLogicalExpression = isLogicalExpression; exports.isBinaryPlus = isBinaryPlus; exports.isUnaryExpression = isUnaryExpression; exports.isArrayExpression = isArrayExpression; exports.isRequireModule = isRequireModule; exports.isMethodInvocation = isMethodInvocation; exports.isFunctionInvocation = isFunctionInvocation; exports.isFunctionCall = isFunctionCall; exports.isMethodCall = isMethodCall; exports.isVariableDeclaration = isVariableDeclaration; exports.isCallingMethod = isCallingMethod; exports.isNamespaceSpecifier = isNamespaceSpecifier; exports.isDefaultSpecifier = isDefaultSpecifier; exports.isModuleExports = isModuleExports; exports.isFunctionNode = isFunctionNode; exports.isLiteral = isLiteral; exports.isNullLiteral = isNullLiteral; exports.isFalseLiteral = isFalseLiteral; exports.isUndefined = isUndefined; exports.isElementWrite = isElementWrite; exports.isReferenceTo = isReferenceTo; exports.getUniqueWriteUsage = getUniqueWriteUsage; exports.getUniqueWriteReference = getUniqueWriteReference; exports.getUniqueWriteUsageOrNode = getUniqueWriteUsageOrNode; exports.getValueOfExpression = getValueOfExpression; exports.getLhsVariable = getLhsVariable; exports.getVariableFromScope = getVariableFromScope; exports.getVariableFromName = getVariableFromName; exports.flattenArgs = flattenArgs; exports.resolveIdentifiers = resolveIdentifiers; exports.getPropertyWithValue = getPropertyWithValue; exports.getProperty = getProperty; exports.resolveFromFunctionReference = resolveFromFunctionReference; exports.resolveFunction = resolveFunction; exports.checkSensitiveCall = checkSensitiveCall; exports.isStringLiteral = isStringLiteral; exports.isBooleanLiteral = isBooleanLiteral; exports.isNumberLiteral = isNumberLiteral; exports.isRegexLiteral = isRegexLiteral; exports.isDotNotation = isDotNotation; exports.isIndexNotation = isIndexNotation; exports.isObjectDestructuring = isObjectDestructuring; exports.isStaticTemplateLiteral = isStaticTemplateLiteral; exports.isSimpleRawString = isSimpleRawString; exports.getSimpleRawStringValue = getSimpleRawStringValue; exports.isThisExpression = isThisExpression; exports.isProperty = isProperty; exports.isUnresolved = isUnresolved; const index_js_1 = require("./index.js"); const MODULE_DECLARATION_NODES = [ 'ImportDeclaration', 'ExportNamedDeclaration', 'ExportDefaultDeclaration', 'ExportAllDeclaration', ]; function isModuleDeclaration(node) { return node !== undefined && MODULE_DECLARATION_NODES.includes(node.type); } exports.FUNCTION_NODES = [ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', ]; exports.functionLike = new Set([ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', 'MethodDefinition', ]); function isFunctionExpression(node) { return node !== undefined && node.type === 'FunctionExpression'; } function isFunctionDeclaration(node) { return node !== undefined && node.type === 'FunctionDeclaration'; } function isArrowFunctionExpression(node) { return node !== undefined && node.type === 'ArrowFunctionExpression'; } function isIdentifier(node, ...values) { return (node?.type === 'Identifier' && (values.length === 0 || values.some(value => value === node.name))); } function getProgramStatements(program) { return program.body.filter((node) => !isModuleDeclaration(node)); } function isIfStatement(node) { return node !== undefined && node.type === 'IfStatement'; } function isMemberWithProperty(node, ...values) { return node.type === 'MemberExpression' && isIdentifier(node.property, ...values); } function isThrowStatement(node) { return node !== undefined && node.type === 'ThrowStatement'; } function isMemberExpression(node, objectValue, ...propertyValue) { if (node.type === 'MemberExpression') { const { object, property } = node; if (isIdentifier(object, objectValue) && isIdentifier(property, ...propertyValue)) { return true; } } return false; } function isLogicalExpression(node) { return node !== undefined && node.type === 'LogicalExpression'; } function isBinaryPlus(node) { return node.type === 'BinaryExpression' && node.operator === '+'; } function isUnaryExpression(node) { return node !== undefined && node.type === 'UnaryExpression'; } function isArrayExpression(node) { return node !== undefined && node.type === 'ArrayExpression'; } function isRequireModule(node, ...moduleNames) { if (isIdentifier(node.callee, 'require') && node.arguments.length === 1) { const argument = node.arguments[0]; if (argument.type === 'Literal') { return moduleNames.includes(String(argument.value)); } } return false; } function isMethodInvocation(callExpression, objectIdentifierName, methodName, minArgs) { return (callExpression.callee.type === 'MemberExpression' && isIdentifier(callExpression.callee.object, objectIdentifierName) && isIdentifier(callExpression.callee.property, methodName) && callExpression.callee.property.type === 'Identifier' && callExpression.arguments.length >= minArgs); } function isFunctionInvocation(callExpression, functionName, minArgs) { return (callExpression.callee.type === 'Identifier' && isIdentifier(callExpression.callee, functionName) && callExpression.arguments.length >= minArgs); } function isFunctionCall(node) { return node.type === 'CallExpression' && node.callee.type === 'Identifier'; } function isMethodCall(callExpr) { return (callExpr.callee.type === 'MemberExpression' && !callExpr.callee.computed && callExpr.callee.property.type === 'Identifier'); } function isVariableDeclaration(node) { return node !== undefined && node.type === 'VariableDeclaration'; } function isCallingMethod(callExpr, arity, ...methodNames) { return (isMethodCall(callExpr) && callExpr.arguments.length === arity && methodNames.includes(callExpr.callee.property.name)); } function isNamespaceSpecifier(importDeclaration, name) { return importDeclaration.specifiers.some(({ type, local }) => type === 'ImportNamespaceSpecifier' && local.name === name); } function isDefaultSpecifier(importDeclaration, name) { return importDeclaration.specifiers.some(({ type, local }) => type === 'ImportDefaultSpecifier' && local.name === name); } function isModuleExports(node) { return (node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'module' && node.property.type === 'Identifier' && node.property.name === 'exports'); } function isFunctionNode(node) { return exports.FUNCTION_NODES.includes(node.type); } // we have similar function in eslint-plugin-sonarjs, however this one accepts null // eventually we should update eslint-plugin-sonarjs function isLiteral(n) { return n != null && n.type === 'Literal'; } function isNullLiteral(n) { return isLiteral(n) && n.value === null; } function isFalseLiteral(n) { return isLiteral(n) && n.value === false; } function isUndefined(node) { return node.type === 'Identifier' && node.name === 'undefined'; } /** * Detect expression statements like the following: * myArray[1] = 42; * myArray[1] += 42; * myObj.prop1 = 3; * myObj.prop1 += 3; */ function isElementWrite(statement, ref, recursive = true) { if (statement.expression.type === 'AssignmentExpression') { const assignmentExpression = statement.expression; const lhs = assignmentExpression.left; return isMemberExpressionReference(lhs, ref, recursive); } return false; } function isMemberExpressionReference(lhs, ref, recursive = true) { return (lhs.type === 'MemberExpression' && (isReferenceTo(ref, lhs.object) || (recursive && isMemberExpressionReference(lhs.object, ref, recursive)))); } function isReferenceTo(ref, node) { return node.type === 'Identifier' && node === ref.identifier; } function getUniqueWriteUsage(context, name, node) { const variable = getVariableFromName(context, name, node); return getUniqueWriteReference(variable); } function getUniqueWriteReference(variable) { if (variable) { const writeReferences = variable.references.filter(reference => reference.isWrite()); if (writeReferences.length === 1 && writeReferences[0].writeExpr) { return writeReferences[0].writeExpr; } } return undefined; } function getUniqueWriteUsageOrNode(context, node, recursive = false) { if (node.type === 'Identifier') { const usage = getUniqueWriteUsage(context, node.name, node); if (usage) { return recursive ? getUniqueWriteUsageOrNode(context, usage, recursive) : usage; } else { return node; } } else { return node; } } function getValueOfExpression(context, expr, type, recursive = false) { if (!expr) { return undefined; } if (isNodeType(expr, type)) { return expr; } if (expr.type === 'Identifier') { const usage = getUniqueWriteUsage(context, expr.name, expr); if (usage) { if (isNodeType(usage, type)) { return usage; } if (recursive) { return getValueOfExpression(context, usage, type, true); } } } return undefined; } // see https://stackoverflow.com/questions/64262105/narrowing-return-value-of-function-based-on-argument function isNodeType(node, type) { return node.type === type; } /** * for `x = 42` or `let x = 42` when visiting '42' returns 'x' variable */ function getLhsVariable(context, node) { const ancestors = context.sourceCode.getAncestors(node); const parent = ancestors[ancestors.length - 1]; let formIdentifier; if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') { formIdentifier = parent.id; } else if (parent.type === 'AssignmentExpression' && parent.left.type === 'Identifier') { formIdentifier = parent.left; } if (formIdentifier) { return getVariableFromName(context, formIdentifier.name, node); } return undefined; } function getVariableFromScope(scope, name) { let variable; while (variable == null && scope != null) { variable = scope.variables.find(value => value.name === name); scope = scope.upper; } return variable; } function getVariableFromName(context, name, node) { const scope = context.sourceCode.getScope(node); return getVariableFromScope(scope, name); } /** * Takes array of arguments. Keeps following variable definitions * and unpacking arrays as long as possible. Returns flattened * array with all collected nodes. * * A usage example should clarify why this might be useful. * According to ExpressJs `app.use` spec, the arguments can be: * * - A middleware function. * - A series of middleware functions (separated by commas). * - An array of middleware functions. * - A combination of all of the above. * * This means that methods like `app.use` accept variable arguments, * but also arrays, or combinations thereof. This methods helps * to flatten out such complicated composed argument lists. */ function flattenArgs(context, args) { // Invokes `getUniqueWriteUsageOrNode` at most once, from then on // only flattens arrays. function recHelper(nodePossiblyIdentifier) { const n = getUniqueWriteUsageOrNode(context, nodePossiblyIdentifier); if (n.type === 'ArrayExpression') { return (0, index_js_1.flatMap)(n.elements, recHelper); } else { return [n]; } } return (0, index_js_1.flatMap)(args, recHelper); } function resolveIdentifiers(node, acceptShorthand = false) { const identifiers = []; resolveIdentifiersAcc(node, identifiers, acceptShorthand); return identifiers; } function resolveIdentifiersAcc(node, identifiers, acceptShorthand) { if (!node) { return; } switch (node.type) { case 'Identifier': identifiers.push(node); break; case 'ObjectPattern': node.properties.forEach(prop => resolveIdentifiersAcc(prop, identifiers, acceptShorthand)); break; case 'ArrayPattern': node.elements.forEach(elem => elem && resolveIdentifiersAcc(elem, identifiers, acceptShorthand)); break; case 'Property': if (acceptShorthand || !node.shorthand) { resolveIdentifiersAcc(node.value, identifiers, acceptShorthand); } break; case 'RestElement': resolveIdentifiersAcc(node.argument, identifiers, acceptShorthand); break; case 'AssignmentPattern': resolveIdentifiersAcc(node.left, identifiers, acceptShorthand); break; case 'TSParameterProperty': resolveIdentifiersAcc(node.parameter, identifiers, acceptShorthand); break; } } function getPropertyWithValue(context, objectExpression, propertyName, propertyValue) { const maybeProperty = getProperty(objectExpression, propertyName, context); if (maybeProperty) { const maybePropertyValue = getValueOfExpression(context, maybeProperty.value, 'Literal'); if (maybePropertyValue?.value === propertyValue) { return maybeProperty; } } return undefined; } function getPropertyFromSpreadElement(spreadElement, key, ctx) { const props = getValueOfExpression(ctx, spreadElement.argument, 'ObjectExpression'); const recursiveDefinition = (0, index_js_1.findFirstMatchingAncestor)(spreadElement.argument, node => node === props); if (recursiveDefinition || props === undefined) { return undefined; } return getProperty(props, key, ctx); } /** * Retrieves the property with the specified key from the given node. * @returns The property if found, or null if not found, or undefined if property not found and one of the properties * is an unresolved SpreadElement. */ function getProperty(expr, key, ctx) { if (expr?.type !== 'ObjectExpression') { return null; } let unresolvedSpreadElement = false; for (let i = expr.properties.length - 1; i >= 0; --i) { const property = expr.properties[i]; if (isProperty(property, key)) { return property; } if (property.type === 'SpreadElement') { const prop = getPropertyFromSpreadElement(property, key, ctx); if (prop === undefined) { unresolvedSpreadElement = true; } else if (prop !== null) { return prop; } } } if (unresolvedSpreadElement) { return undefined; } return null; function isProperty(node, key) { return (node.type === 'Property' && (isIdentifier(node.key, key) || (isStringLiteral(node.key) && node.key.value === key))); } } function resolveFromFunctionReference(context, functionIdentifier) { const { scopeManager } = context.sourceCode; for (const scope of scopeManager.scopes) { const reference = scope.references.find(r => r.identifier === functionIdentifier); if (reference?.resolved && reference.resolved.defs.length === 1 && reference.resolved.defs[0].type === 'FunctionName') { return reference.resolved.defs[0].node; } } return null; } function resolveFunction(context, node) { if (isFunctionNode(node)) { return node; } else if (node.type === 'Identifier') { return resolveFromFunctionReference(context, node); } else { return null; } } function checkSensitiveCall(context, callExpression, sensitiveArgumentIndex, sensitiveProperty, sensitivePropertyValue, message) { if (callExpression.arguments.length < sensitiveArgumentIndex + 1) { return; } const sensitiveArgument = callExpression.arguments[sensitiveArgumentIndex]; const options = getValueOfExpression(context, sensitiveArgument, 'ObjectExpression'); if (!options) { return; } const unsafeProperty = getPropertyWithValue(context, options, sensitiveProperty, sensitivePropertyValue); if (unsafeProperty) { (0, index_js_1.report)(context, { node: callExpression.callee, message, }, [(0, index_js_1.toSecondaryLocation)(unsafeProperty)]); } } function isStringLiteral(node) { return isLiteral(node) && typeof node.value === 'string'; } function isBooleanLiteral(node) { return isLiteral(node) && typeof node.value === 'boolean'; } function isNumberLiteral(node) { return isLiteral(node) && typeof node.value === 'number'; } function isRegexLiteral(node) { return node.type === 'Literal' && node.value instanceof RegExp; } /** * Checks if the node is of the form: foo.bar * * @param node * @returns */ function isDotNotation(node) { return node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier'; } /** * Checks if the node is of the form: foo["bar"] * * @param node * @returns */ function isIndexNotation(node) { return node.type === 'MemberExpression' && node.computed && isStringLiteral(node.property); } function isObjectDestructuring(node) { return ((node.type === 'VariableDeclarator' && node.id.type === 'ObjectPattern') || (node.type === 'AssignmentExpression' && node.left.type === 'ObjectPattern')); } function isStaticTemplateLiteral(node) { return (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1); } // Test for raw expressions like: String.raw`c:\foo\bar.txt` that corresponds to 'c:\\foo\\bar.txt' function isSimpleRawString(node) { return (node.type === 'TaggedTemplateExpression' && isDotNotation(node.tag) && isIdentifier(node.tag.object, 'String') && isIdentifier(node.tag.property, 'raw') && isStaticTemplateLiteral(node.quasi)); } // In simple raw strings, the literal value is: node.quasi.quasis[0].value.raw // This function fails if isSimpleRawString() is not returning true for the node. function getSimpleRawStringValue(node) { return node.quasi.quasis[0].value.raw; } function isThisExpression(node) { return node.type === 'ThisExpression'; } function isProperty(node) { return node.type === 'Property'; } /** * Check if an identifier has no known value, meaning: * * - It's not imported/required * - Defined variable without any write references (function parameter?) * - Non-defined variable (a possible global?) * * @param node Node to check * @param ctx Rule context */ function isUnresolved(node, ctx) { if (!node || (0, index_js_1.getFullyQualifiedName)(ctx, node) || isUndefined(node)) { return false; } let nodeToCheck = node; while (nodeToCheck.type === 'MemberExpression') { nodeToCheck = nodeToCheck.object; } if (nodeToCheck.type === 'Identifier') { const variable = getVariableFromName(ctx, nodeToCheck.name, node); const writeReferences = variable?.references.filter(reference => reference.isWrite()); if (!variable || !writeReferences?.length) { return true; } } return false; }