diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 6b4f4d559..4d226b1be 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -9,6 +9,10 @@ try { typescriptPkg = require('typescript/package.json'); // eslint-disable-line import/no-extraneous-dependencies } catch (e) { /**/ } +function getSourceFromNode(node) { + return node.callee ? node.arguments[0] : node.source; +} + function checkImports(imported, context) { for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { @@ -18,14 +22,14 @@ function checkImports(imported, context) { const fix = getFix(first, rest, sourceCode, context); context.report({ - node: first.source, + node: getSourceFromNode(first), message, fix, // Attach the autofix (if any) to the first import. }); for (const node of rest) { context.report({ - node: node.source, + node: getSourceFromNode(node), message, }); } @@ -44,6 +48,12 @@ function getFix(first, rest, sourceCode, context) { return undefined; } + // if there is a require call in any of the duplicates, return undefined + // this could be implemented in the future + if ([first, ...rest].some((node) => 'callee' in node)) { + return undefined; + } + // Adjusting the first import might make it multiline, which could break // `eslint-disable-next-line` comments and similar, so bail if the first // import has comments. Also, if the first import is `import * as ns from @@ -307,16 +317,27 @@ module.exports = { const moduleMaps = new Map(); - function getImportMap(n) { - if (!moduleMaps.has(n.parent)) { - moduleMaps.set(n.parent, { + function getPrimalRef(n) { + if (n.type === 'Program' || n.type === 'DeclareModule' || n.type === 'TSModuleBlock') { return n; } + return getPrimalRef(n.parent); + } + + function getModuleMaps(n) { + const marker = getPrimalRef(n); + if (!moduleMaps.has(marker)) { + moduleMaps.set(marker, { imported: new Map(), nsImported: new Map(), defaultTypesImported: new Map(), namedTypesImported: new Map(), }); } - const map = moduleMaps.get(n.parent); + const map = moduleMaps.get(marker); + return map; + } + + function getImportMap(n) { + const map = getModuleMaps(n); const preferInline = context.options[0] && context.options[0]['prefer-inline']; if (!preferInline && n.importKind === 'type') { return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported; @@ -341,6 +362,21 @@ module.exports = { } }, + CallExpression(call) { + if (call.callee.name !== 'require' || call.callee.type !== 'Identifier' || call.arguments.length !== 1) { return; } + const modulePath = call.arguments[0]; + if (modulePath.type !== 'Literal') { return; } + + const resolvedPath = resolver(modulePath.value); + const importMap = getModuleMaps(call).imported; + + if (importMap.has(resolvedPath)) { + importMap.get(resolvedPath).push(call); + } else { + importMap.set(resolvedPath, [call]); + } + }, + 'Program:exit'() { for (const map of moduleMaps.values()) { checkImports(map.imported, context); diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index f83221105..89d96b096 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -49,8 +49,27 @@ ruleTester.run('no-duplicates', rule, { test({ code: "import {y} from './foo'; import * as ns from './foo'", }), + + // require imports + test({ + code: "const a = require('./foo');", + }), + test({ + code: "const a = require('./foo'); const b = require('./bar');", + }), ], invalid: [ + // require imports + test({ + code: "const a = require('./foo'); const b = require('./foo');", + errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], + }), + + test({ + code: "const a = require('./foo'); const b = require('./bar'); const c = require('./foo');", + errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], + }), + test({ code: "import { x } from './foo'; import { y } from './foo'", output: "import { x , y } from './foo'; ",