UNPKG

eslint-plugin-sonarjs

Version:
502 lines (501 loc) • 18.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getImportDeclarations = getImportDeclarations; exports.isGlobalShadowed = isGlobalShadowed; exports.clearFileCaches = clearFileCaches; exports.getCurrentFileImports = getCurrentFileImports; exports.getCurrentFileModuleReferences = getCurrentFileModuleReferences; 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(), }; const CURRENT_FILE_MODULE_REFERENCES = { sourceCode: null, references: new Set(), }; /** * Compute the imports used by dependency-sensitive rules. Keep this intentionally narrower than * telemetry collection to avoid changing rule activation semantics. */ function computeCurrentFileImports(sourceCode) { if (CURRENT_FILE_IMPORTS.sourceCode === sourceCode) { return; } CURRENT_FILE_IMPORTS.sourceCode = sourceCode; CURRENT_FILE_IMPORTS.imports.clear(); collectStaticImports(sourceCode); collectScopedVariableImports(sourceCode); } function collectStaticImports(sourceCode) { for (const node of sourceCode.ast.body) { if (node.type === 'ImportDeclaration' && typeof node.source.value === 'string') { CURRENT_FILE_IMPORTS.imports.add(node.source.value); } } } function collectScopedVariableImports(sourceCode) { for (const scope of sourceCode.scopeManager.scopes) { collectScopeImports(scope); } } function collectScopeImports(scope) { for (const variable of scope.variables) { collectVariableImports(variable); } } function collectVariableImports(variable) { for (const def of variable.defs) { if (def.type !== 'Variable' || !def.node.init) { continue; } const name = getRequireModuleName(def.node.init) ?? getDynamicImportModuleName(def.node.init); if (name !== undefined) { CURRENT_FILE_IMPORTS.imports.add(name); } } } /** * Compute all literal module references needed by package-import telemetry. * * Supported patterns: * import foo from 'lodash'; * const lodash = require('lodash'); * const partition = require('lodash').partition; * const lodash = import('lodash'); * const lodash = await import('lodash'); * * export { partition } from 'lodash'; * export * from 'lodash'; * import lodash = require('lodash'); * require('lodash'); * import('lodash').then(lodash => ...); * type Props = import('lodash').SomeType; */ function computeCurrentFileModuleReferences(sourceCode) { if (CURRENT_FILE_MODULE_REFERENCES.sourceCode === sourceCode) { return; } CURRENT_FILE_MODULE_REFERENCES.sourceCode = sourceCode; CURRENT_FILE_MODULE_REFERENCES.references.clear(); const pending = [sourceCode.ast]; while (pending.length > 0) { const node = pending.pop(); if (node === undefined) { break; } const moduleName = getReferencedModuleName(sourceCode, node); if (moduleName !== undefined) { CURRENT_FILE_MODULE_REFERENCES.references.add(moduleName); } for (const visitorKey of sourceCode.visitorKeys[node.type] ?? []) { addChildNodes(pending, node[visitorKey]); } } } function addChildNodes(pending, child) { if (Array.isArray(child)) { for (const item of child) { addNode(pending, item); } } else { addNode(pending, child); } } function addNode(pending, value) { if (isNode(value)) { pending.push(value); } } function getReferencedModuleName(sourceCode, node) { if ((node.type === 'ImportDeclaration' || node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') && node.source && typeof node.source.value === 'string') { return node.source.value; } if (node.type === 'TSImportEqualsDeclaration') { const moduleReference = node.moduleReference; if (moduleReference.type === 'TSExternalModuleReference' && moduleReference.expression.type === 'Literal' && typeof moduleReference.expression.value === 'string') { return moduleReference.expression.value; } } if (node.type === 'TSImportType') { return node.source.value; } return getUnshadowedRequireModuleName(sourceCode, node) ?? getDynamicImportModuleName(node); } function getUnshadowedRequireModuleName(sourceCode, node) { const requireCall = getRequireCall(node); if (requireCall === undefined) { return undefined; } if (isGlobalShadowed(sourceCode, requireCall, 'require')) { return undefined; } const moduleName = requireCall.arguments[0]; return moduleName?.type === 'Literal' && typeof moduleName.value === 'string' ? moduleName.value : undefined; } /** * True when `name` resolves to a local binding in the scope of `node`, e.g., a test double named * `require`, a local `function define(...) {}` or a mocked `sap` object, rather than to the * ambient global of that name. */ function isGlobalShadowed(sourceCode, node, name) { return !!(0, ast_js_1.getVariableFromScope)(sourceCode.getScope(node), name)?.defs.length; } function isNode(value) { return (typeof value === 'object' && value !== null && typeof value.type === 'string'); } /** * Clears the caches related to the given source file */ function clearFileCaches() { CURRENT_FILE_IMPORTS.sourceCode = null; CURRENT_FILE_IMPORTS.imports.clear(); CURRENT_FILE_MODULE_REFERENCES.sourceCode = null; CURRENT_FILE_MODULE_REFERENCES.references.clear(); (0, dependencies_js_1.setCurrentFileInlineDependencies)(null); } function getCurrentFileImports(sourceCode) { computeCurrentFileImports(sourceCode); return CURRENT_FILE_IMPORTS.imports; } function getCurrentFileModuleReferences(sourceCode) { computeCurrentFileModuleReferences(sourceCode); return CURRENT_FILE_MODULE_REFERENCES.references; } 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') { return null; } const specifier = definition.node; const importDeclaration = definition.parent; if (isTypeOnlyImportBinding(specifier, importDeclaration)) { return null; } addImportSpecifierQualifier(specifier, fqn); if (typeof importDeclaration?.source?.value === 'string') { const importedQualifiers = importDeclaration.source.value.split('/'); fqn.unshift(...importedQualifiers); return fqn.join('.'); } return checkFqnFromTsImportEquals(variable, importDeclaration, context, fqn, visitedVars); } function addImportSpecifierQualifier(specifier, fqn) { if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name !== 'default') { fqn.unshift(specifier.imported.name); } } function checkFqnFromTsImportEquals(variable, importDeclaration, context, fqn, visitedVars) { if (importDeclaration?.type !== 'TSImportEqualsDeclaration') { return null; } 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('.'); } if (importedModule.type === 'TSQualifiedName') { visitedVars.push(variable); return getFullyQualifiedNameRaw(context, importedModule, fqn, variable.scope, visitedVars); } return null; } function isTypeOnlyImportBinding(specifier, importDeclaration) { return ((importDeclaration?.type === 'ImportDeclaration' && (0, ast_js_1.isTypeOnlyImport)(importDeclaration)) || (specifier.type === 'ImportSpecifier' && specifier.importKind === 'type') || (importDeclaration?.type === 'TSImportEqualsDeclaration' && importDeclaration.importKind === 'type')); } 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') { nodeToCheck = reduceMemberExpression(nodeToCheck, fqn); } 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; } function reduceMemberExpression(node, fqn) { const { property } = node; if (property.type === 'Literal' && typeof property.value === 'string') { fqn.unshift(property.value); } else if (property.type === 'Identifier') { fqn.unshift(property.name); } return node.object; }