UNPKG

eslint-plugin-sonarjs

Version:
375 lines (374 loc) 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getImportDeclarations = getImportDeclarations; exports.clearFileCaches = clearFileCaches; exports.getCurrentFileImports = getCurrentFileImports; exports.importsModule = importsModule; exports.importsOrDependsOnModule = importsOrDependsOnModule; exports.isRequire = isRequire; exports.getFullyQualifiedName = getFullyQualifiedName; exports.removeNodePrefixIfExists = removeNodePrefixIfExists; exports.reduceToIdentifier = reduceToIdentifier; const ast_js_1 = require("./ast.js"); const dependencies_js_1 = require("./dependency-manifests/dependencies.js"); function getImportDeclarations(context) { return context.sourceCode.ast.body.filter(node => node.type === 'ImportDeclaration'); } /** * Cache for storing the set of all module names imported or required in the file currently being analyzed. * This set is only needed while the current file is being analyzed. * * Keyed by the `sourceCode` object reference rather than a boolean flag so that the cache * self-invalidates when ESLint's RuleTester switches between test cases (each case gets a fresh * SourceCode instance). */ const CURRENT_FILE_IMPORTS = { sourceCode: null, imports: new Set(), }; /** * Compute the set of all module names imported or required * in the file currently being analyzed (via import declarations, require(), or dynamic import()). * * Supported patterns: * import foo from 'lodash'; * const lodash = require('lodash'); * const partition = require('lodash').partition; * const lodash = import('lodash'); * const lodash = await import('lodash'); * * Unsupported patterns (not stored in a variable): * require('lodash'); * import('lodash').then(lodash => ...); */ function computeCurrentFileImports(sourceCode) { if (CURRENT_FILE_IMPORTS.sourceCode === sourceCode) { return; } CURRENT_FILE_IMPORTS.sourceCode = sourceCode; CURRENT_FILE_IMPORTS.imports.clear(); for (const node of sourceCode.ast.body) { if (node.type === 'ImportDeclaration' && typeof node.source.value === 'string') { CURRENT_FILE_IMPORTS.imports.add(node.source.value); } } for (const scope of sourceCode.scopeManager.scopes) { for (const variable of scope.variables) { for (const def of variable.defs) { if (def.type === 'Variable' && def.node.init) { const name = getRequireModuleName(def.node.init) ?? getDynamicImportModuleName(def.node.init); if (name !== undefined) { CURRENT_FILE_IMPORTS.imports.add(name); } } } } } } /** * Clears the caches related to the given source file */ function clearFileCaches() { CURRENT_FILE_IMPORTS.sourceCode = null; CURRENT_FILE_IMPORTS.imports.clear(); (0, dependencies_js_1.setCurrentFileInlineDependencies)(null); } function getCurrentFileImports(sourceCode) { computeCurrentFileImports(sourceCode); return CURRENT_FILE_IMPORTS.imports; } function getRequireModuleName(node) { const requireCall = getRequireCall(node); const moduleName = requireCall?.arguments[0]; if (moduleName?.type === 'Literal' && typeof moduleName.value === 'string') { return moduleName.value; } return undefined; } function getRequireCall(node) { if (isRequire(node)) { return node; } if (node.type === 'MemberExpression' && isRequire(node.object)) { return node.object; } return undefined; } function getDynamicImportModuleName(node) { const dynamicImportExpression = getDynamicImportExpression(node); const moduleName = dynamicImportExpression?.source; if (moduleName?.type === 'Literal' && typeof moduleName.value === 'string') { return moduleName.value; } return undefined; } function getDynamicImportExpression(node) { if (node.type === 'ImportExpression') { return node; } if (node.type === 'AwaitExpression' && node.argument.type === 'ImportExpression') { return node.argument; } return undefined; } /** * Checks if the file imports any of the specified modules, either via require() calls or import declarations. */ function importsModule(context, moduleNames) { if (moduleNames.length === 0) { return false; } const imports = getCurrentFileImports(context.sourceCode); return moduleNames.some(name => imports.has(name)); } /** * Checks if at least one of the specified imports is imported (via import or require) in the file * or if at least one of the specified dependencies is listed in the corresponding dependency manifest. */ function importsOrDependsOnModule(context, imports, dependencies) { if (importsModule(context, imports)) { return true; } const sanitizedDeps = (0, dependencies_js_1.getDependenciesSanitizePaths)(context); return dependencies.some(dependency => sanitizedDeps.has(dependency)); } function isRequire(node) { return (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require' && node.arguments.length === 1); } /** * Returns 'module' if `node` is a `require('module')` CallExpression * * For usage inside rules, prefer getFullyQualifiedName() * * @param node * @returns the module name or undefined */ function getModuleNameFromRequire(node) { if (isRequire(node)) { const moduleName = node.arguments[0]; if (moduleName.type === 'Literal') { return moduleName; } } return undefined; } /** * Returns the fully qualified name of ESLint node * * This function filters out the `node:` prefix * * A fully qualified name here denotes a value that is accessed through an imported * symbol, e.g., `foo.bar.baz` where `foo` was imported either from a require call * or an import statement: * * ``` * const foo = require('lib'); * foo.bar.baz.qux; // matches the fully qualified name 'lib.bar.baz.qux' (not 'foo.bar.baz.qux') * const foo2 = require('lib').bar; * foo2.baz.qux; // matches the fully qualified name 'lib.bar.baz.qux' * ``` * * Returns null when an FQN could not be found. * * @param context the rule context * @param node the node * @param fqn the already traversed FQN (for recursive calls) * @param scope scope to look for the variable definition, used in recursion not to * loop over same variable always in the lower scope */ function getFullyQualifiedName(context, node, fqn = [], scope) { return removeNodePrefixIfExists(getFullyQualifiedNameRaw(context, node, fqn, scope)); } /** * Just like getFullyQualifiedName(), but does not filter out the `node:` prefix. * * To be used for rules that need to work with the `node:` prefix. */ function getFullyQualifiedNameRaw(context, node, fqn, scope, visitedVars = []) { const nodeToCheck = reduceToIdentifier(node, fqn); if (!(0, ast_js_1.isIdentifier)(nodeToCheck)) { // require chaining, e.g. `require('lib')()` or `require('lib').prop()` if (node.type === 'CallExpression') { const qualifiers = []; const maybeRequire = reduceTo('CallExpression', node.callee, qualifiers); const module = getModuleNameFromRequire(maybeRequire); if (typeof module?.value === 'string') { qualifiers.unshift(module.value); return qualifiers.join('.'); } } return null; } const variable = (0, ast_js_1.getVariableFromScope)(scope ?? context.sourceCode.getScope(node), nodeToCheck.name); if (!variable || variable.defs.length > 1) { return null; } // built-in variable // ESLint marks built-in global variables with an undocumented hidden `writeable` property that should equal `false`. // @see https://github.com/eslint/eslint/blob/6380c87c563be5dc78ce0ddd5c7409aaf71692bb/lib/linter/linter.js#L207 // @see https://github.com/eslint/eslint/blob/6380c87c563be5dc78ce0ddd5c7409aaf71692bb/lib/rules/no-global-assign.js#L81 if (variable.writeable === false || visitedVars.includes(variable)) { fqn.unshift(nodeToCheck.name); return fqn.join('.'); } const definition = variable.defs.find(({ type }) => ['ImportBinding', 'Variable'].includes(type)); if (!definition) { return null; } // imports const fqnFromImport = checkFqnFromImport(variable, definition, context, fqn, visitedVars); if (fqnFromImport !== null) { return fqnFromImport; } // requires const fqnFromRequire = checkFqnFromRequire(variable, definition, context, fqn, visitedVars); if (fqnFromRequire !== null) { return fqnFromRequire; } return null; } function checkFqnFromImport(variable, definition, context, fqn, visitedVars) { if (definition.type === 'ImportBinding') { const specifier = definition.node; const importDeclaration = definition.parent; // import {default as cdk} from 'aws-cdk-lib'; // vs. // import { aws_s3 as s3 } from 'aws-cdk-lib'; if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported?.name !== 'default') { fqn.unshift(specifier.imported?.name); } if (typeof importDeclaration.source?.value === 'string') { const importedQualifiers = importDeclaration.source.value.split('/'); fqn.unshift(...importedQualifiers); return fqn.join('.'); } // example: import s3 = require('aws-cdk-lib/aws-s3'); if (importDeclaration.type === 'TSImportEqualsDeclaration') { const importedModule = importDeclaration .moduleReference; if (importedModule.type === 'TSExternalModuleReference' && importedModule.expression.type === 'Literal' && typeof importedModule.expression.value === 'string') { const importedQualifiers = importedModule.expression.value.split('/'); fqn.unshift(...importedQualifiers); return fqn.join('.'); } // example: import s3 = cdk.aws_s3; if (importedModule.type === 'TSQualifiedName') { visitedVars.push(variable); return getFullyQualifiedNameRaw(context, importedModule, fqn, variable.scope, visitedVars); } } } return null; } function checkFqnFromRequire(variable, definition, context, fqn, visitedVars) { const value = (0, ast_js_1.getUniqueWriteReference)(variable); // requires if (definition.type === 'Variable' && value) { // case for `const {Bucket} = require('aws-cdk-lib/aws-s3');` // case for `const {Bucket: foo} = require('aws-cdk-lib/aws-s3');` if (definition.node.id.type === 'ObjectPattern') { for (const property of definition.node.id.properties) { if (property.value === definition.name) { fqn.unshift(property.key.name); } } } const nodeToCheck = reduceTo('CallExpression', value, fqn); const module = getModuleNameFromRequire(nodeToCheck)?.value; if (typeof module === 'string') { const importedQualifiers = module.split('/'); fqn.unshift(...importedQualifiers); return fqn.join('.'); } else { visitedVars.push(variable); return getFullyQualifiedNameRaw(context, nodeToCheck, fqn, variable.scope, visitedVars); } } return null; } /** * Removes `node:` prefix if such exists * * Node.js builtin modules can be referenced with a `node:` prefix (eg.: node:fs/promises) * * https://nodejs.org/api/esm.html#node-imports * * @param fqn Fully Qualified Name (ex.: `node:https.request`) * @returns `fqn` sanitized from `node:` prefix (ex.: `https.request`) */ function removeNodePrefixIfExists(fqn) { if (fqn === null) { return null; } const NODE_NAMESPACE = 'node:'; if (fqn.startsWith(NODE_NAMESPACE)) { return fqn.substring(NODE_NAMESPACE.length); } return fqn; } /** * Helper function for getFullyQualifiedName to handle Member expressions * filling in the FQN array with the accessed properties. * @param node the Node to traverse * @param fqn the array with the qualifiers */ function reduceToIdentifier(node, fqn = []) { return reduceTo('Identifier', node, fqn); } /** * Reduce a given node through its ancestors until a given node type is found * filling in the FQN array with the accessed properties. * @param type the type of node you are looking for to be returned. Returned node still needs to be * checked as its type it's not guaranteed to match the passed type. * @param node the Node to traverse * @param fqn the array with the qualifiers */ function reduceTo(type, node, fqn = []) { let nodeToCheck = node; while (nodeToCheck.type !== type) { if (nodeToCheck.type === 'MemberExpression') { const { property } = nodeToCheck; if (property.type === 'Literal' && typeof property.value === 'string') { fqn.unshift(property.value); } else if (property.type === 'Identifier') { fqn.unshift(property.name); } nodeToCheck = nodeToCheck.object; } else if (nodeToCheck.type === 'CallExpression' && !getModuleNameFromRequire(nodeToCheck)) { nodeToCheck = nodeToCheck.callee; } else if (nodeToCheck.type === 'TaggedTemplateExpression') { nodeToCheck = nodeToCheck.tag; } else if (nodeToCheck.type === 'NewExpression') { nodeToCheck = nodeToCheck.callee; } else if (nodeToCheck.type === 'ChainExpression') { nodeToCheck = nodeToCheck.expression; } else if (nodeToCheck.type === 'TSNonNullExpression') { // we should migrate to use only TSESTree types everywhere to avoid casting nodeToCheck = nodeToCheck .expression; } else if (nodeToCheck.type === 'TSQualifiedName') { const qualified = nodeToCheck; fqn.unshift(qualified.right.name); nodeToCheck = qualified.left; } else { break; } } return nodeToCheck; }