Path: blob/main/src/rules/no-duplicates.js
829 views
import resolve from 'eslint-module-utils/resolve';1import docsUrl from '../docsUrl';23function checkImports(imported, context) {4for (const [module, nodes] of imported.entries()) {5if (nodes.length > 1) {6const message = `'${module}' imported multiple times.`;7const [first, ...rest] = nodes;8const sourceCode = context.getSourceCode();9const fix = getFix(first, rest, sourceCode);1011context.report({12node: first.source,13message,14fix, // Attach the autofix (if any) to the first import.15});1617for (const node of rest) {18context.report({19node: node.source,20message,21});22}23}24}25}2627function getFix(first, rest, sourceCode) {28// Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports29// requires multiple `fixer.whatever()` calls in the `fix`: We both need to30// update the first one, and remove the rest. Support for multiple31// `fixer.whatever()` in a single `fix` was added in ESLint 4.1.32// `sourceCode.getCommentsBefore` was added in 4.0, so that's an easy thing to33// check for.34if (typeof sourceCode.getCommentsBefore !== 'function') {35return undefined;36}3738// Adjusting the first import might make it multiline, which could break39// `eslint-disable-next-line` comments and similar, so bail if the first40// import has comments. Also, if the first import is `import * as ns from41// './foo'` there's nothing we can do.42if (hasProblematicComments(first, sourceCode) || hasNamespace(first)) {43return undefined;44}4546const defaultImportNames = new Set(47[first, ...rest].map(getDefaultImportName).filter(Boolean),48);4950// Bail if there are multiple different default import names – it's up to the51// user to choose which one to keep.52if (defaultImportNames.size > 1) {53return undefined;54}5556// Leave it to the user to handle comments. Also skip `import * as ns from57// './foo'` imports, since they cannot be merged into another import.58const restWithoutComments = rest.filter(node => !(59hasProblematicComments(node, sourceCode) ||60hasNamespace(node)61));6263const specifiers = restWithoutComments64.map(node => {65const tokens = sourceCode.getTokens(node);66const openBrace = tokens.find(token => isPunctuator(token, '{'));67const closeBrace = tokens.find(token => isPunctuator(token, '}'));6869if (openBrace == null || closeBrace == null) {70return undefined;71}7273return {74importNode: node,75text: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]),76hasTrailingComma: isPunctuator(sourceCode.getTokenBefore(closeBrace), ','),77isEmpty: !hasSpecifiers(node),78};79})80.filter(Boolean);8182const unnecessaryImports = restWithoutComments.filter(node =>83!hasSpecifiers(node) &&84!hasNamespace(node) &&85!specifiers.some(specifier => specifier.importNode === node),86);8788const shouldAddDefault = getDefaultImportName(first) == null && defaultImportNames.size === 1;89const shouldAddSpecifiers = specifiers.length > 0;90const shouldRemoveUnnecessary = unnecessaryImports.length > 0;9192if (!(shouldAddDefault || shouldAddSpecifiers || shouldRemoveUnnecessary)) {93return undefined;94}9596return fixer => {97const tokens = sourceCode.getTokens(first);98const openBrace = tokens.find(token => isPunctuator(token, '{'));99const closeBrace = tokens.find(token => isPunctuator(token, '}'));100const firstToken = sourceCode.getFirstToken(first);101const [defaultImportName] = defaultImportNames;102103const firstHasTrailingComma =104closeBrace != null &&105isPunctuator(sourceCode.getTokenBefore(closeBrace), ',');106const firstIsEmpty = !hasSpecifiers(first);107108const [specifiersText] = specifiers.reduce(109([result, needsComma], specifier) => {110return [111needsComma && !specifier.isEmpty112? `${result},${specifier.text}`113: `${result}${specifier.text}`,114specifier.isEmpty ? needsComma : true,115];116},117['', !firstHasTrailingComma && !firstIsEmpty],118);119120const fixes = [];121122if (shouldAddDefault && openBrace == null && shouldAddSpecifiers) {123// `import './foo'` → `import def, {...} from './foo'`124fixes.push(125fixer.insertTextAfter(firstToken, ` ${defaultImportName}, {${specifiersText}} from`),126);127} else if (shouldAddDefault && openBrace == null && !shouldAddSpecifiers) {128// `import './foo'` → `import def from './foo'`129fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName} from`));130} else if (shouldAddDefault && openBrace != null && closeBrace != null) {131// `import {...} from './foo'` → `import def, {...} from './foo'`132fixes.push(fixer.insertTextAfter(firstToken, ` ${defaultImportName},`));133if (shouldAddSpecifiers) {134// `import def, {...} from './foo'` → `import def, {..., ...} from './foo'`135fixes.push(fixer.insertTextBefore(closeBrace, specifiersText));136}137} else if (!shouldAddDefault && openBrace == null && shouldAddSpecifiers) {138if (first.specifiers.length === 0) {139// `import './foo'` → `import {...} from './foo'`140fixes.push(fixer.insertTextAfter(firstToken, ` {${specifiersText}} from`));141} else {142// `import def from './foo'` → `import def, {...} from './foo'`143fixes.push(fixer.insertTextAfter(first.specifiers[0], `, {${specifiersText}}`));144}145} else if (!shouldAddDefault && openBrace != null && closeBrace != null) {146// `import {...} './foo'` → `import {..., ...} from './foo'`147fixes.push(fixer.insertTextBefore(closeBrace, specifiersText));148}149150// Remove imports whose specifiers have been moved into the first import.151for (const specifier of specifiers) {152const importNode = specifier.importNode;153fixes.push(fixer.remove(importNode));154155const charAfterImportRange = [importNode.range[1], importNode.range[1] + 1];156const charAfterImport = sourceCode.text.substring(charAfterImportRange[0], charAfterImportRange[1]);157if (charAfterImport === '\n') {158fixes.push(fixer.removeRange(charAfterImportRange));159}160}161162// Remove imports whose default import has been moved to the first import,163// and side-effect-only imports that are unnecessary due to the first164// import.165for (const node of unnecessaryImports) {166fixes.push(fixer.remove(node));167168const charAfterImportRange = [node.range[1], node.range[1] + 1];169const charAfterImport = sourceCode.text.substring(charAfterImportRange[0], charAfterImportRange[1]);170if (charAfterImport === '\n') {171fixes.push(fixer.removeRange(charAfterImportRange));172}173}174175return fixes;176};177}178179function isPunctuator(node, value) {180return node.type === 'Punctuator' && node.value === value;181}182183// Get the name of the default import of `node`, if any.184function getDefaultImportName(node) {185const defaultSpecifier = node.specifiers186.find(specifier => specifier.type === 'ImportDefaultSpecifier');187return defaultSpecifier != null ? defaultSpecifier.local.name : undefined;188}189190// Checks whether `node` has a namespace import.191function hasNamespace(node) {192const specifiers = node.specifiers193.filter(specifier => specifier.type === 'ImportNamespaceSpecifier');194return specifiers.length > 0;195}196197// Checks whether `node` has any non-default specifiers.198function hasSpecifiers(node) {199const specifiers = node.specifiers200.filter(specifier => specifier.type === 'ImportSpecifier');201return specifiers.length > 0;202}203204// It's not obvious what the user wants to do with comments associated with205// duplicate imports, so skip imports with comments when autofixing.206function hasProblematicComments(node, sourceCode) {207return (208hasCommentBefore(node, sourceCode) ||209hasCommentAfter(node, sourceCode) ||210hasCommentInsideNonSpecifiers(node, sourceCode)211);212}213214// Checks whether `node` has a comment (that ends) on the previous line or on215// the same line as `node` (starts).216function hasCommentBefore(node, sourceCode) {217return sourceCode.getCommentsBefore(node)218.some(comment => comment.loc.end.line >= node.loc.start.line - 1);219}220221// Checks whether `node` has a comment (that starts) on the same line as `node`222// (ends).223function hasCommentAfter(node, sourceCode) {224return sourceCode.getCommentsAfter(node)225.some(comment => comment.loc.start.line === node.loc.end.line);226}227228// Checks whether `node` has any comments _inside,_ except inside the `{...}`229// part (if any).230function hasCommentInsideNonSpecifiers(node, sourceCode) {231const tokens = sourceCode.getTokens(node);232const openBraceIndex = tokens.findIndex(token => isPunctuator(token, '{'));233const closeBraceIndex = tokens.findIndex(token => isPunctuator(token, '}'));234// Slice away the first token, since we're no looking for comments _before_235// `node` (only inside). If there's a `{...}` part, look for comments before236// the `{`, but not before the `}` (hence the `+1`s).237const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0238? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1))239: tokens.slice(1);240return someTokens.some(token => sourceCode.getCommentsBefore(token).length > 0);241}242243module.exports = {244meta: {245type: 'problem',246docs: {247url: docsUrl('no-duplicates'),248},249fixable: 'code',250schema: [251{252type: 'object',253properties: {254considerQueryString: {255type: 'boolean',256},257},258additionalProperties: false,259},260],261},262263create(context) {264// Prepare the resolver from options.265const considerQueryStringOption = context.options[0] &&266context.options[0]['considerQueryString'];267const defaultResolver = sourcePath => resolve(sourcePath, context) || sourcePath;268const resolver = considerQueryStringOption ? (sourcePath => {269const parts = sourcePath.match(/^([^?]*)\?(.*)$/);270if (!parts) {271return defaultResolver(sourcePath);272}273return defaultResolver(parts[1]) + '?' + parts[2];274}) : defaultResolver;275276const imported = new Map();277const nsImported = new Map();278const defaultTypesImported = new Map();279const namedTypesImported = new Map();280281function getImportMap(n) {282if (n.importKind === 'type') {283return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? defaultTypesImported : namedTypesImported;284}285286return hasNamespace(n) ? nsImported : imported;287}288289return {290ImportDeclaration(n) {291// resolved path will cover aliased duplicates292const resolvedPath = resolver(n.source.value);293const importMap = getImportMap(n);294295if (importMap.has(resolvedPath)) {296importMap.get(resolvedPath).push(n);297} else {298importMap.set(resolvedPath, [n]);299}300},301302'Program:exit': function () {303checkImports(imported, context);304checkImports(nsImported, context);305checkImports(defaultTypesImported, context);306checkImports(namedTypesImported, context);307},308};309},310};311312313