-
-
Notifications
You must be signed in to change notification settings - Fork 384
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <[email protected]>
- Loading branch information
1 parent
2985ecc
commit ed8da1b
Showing
11 changed files
with
923 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# Disallow named usage of default import and export | ||
|
||
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs). | ||
|
||
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
|
||
<!-- end auto-generated rule header --> | ||
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` --> | ||
|
||
Enforces the use of the [`default import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) and [`default export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#using_the_default_export) syntax instead of named syntax. | ||
|
||
## Examples | ||
|
||
```js | ||
// ❌ | ||
import {default as foo} from 'foo'; | ||
|
||
// ✅ | ||
import foo from 'foo'; | ||
``` | ||
|
||
```js | ||
// ❌ | ||
import {default as foo, bar} from 'foo'; | ||
|
||
// ✅ | ||
import foo, {bar} from 'foo'; | ||
``` | ||
|
||
```js | ||
// ❌ | ||
export {foo as default}; | ||
|
||
// ✅ | ||
export default foo; | ||
``` | ||
|
||
```js | ||
// ❌ | ||
export {foo as default, bar}; | ||
|
||
// ✅ | ||
export default foo; | ||
export {bar}; | ||
``` | ||
|
||
```js | ||
// ❌ | ||
import foo, {default as anotherFoo} from 'foo'; | ||
|
||
function bar(foo) { | ||
doSomeThing(anotherFoo, foo); | ||
} | ||
|
||
// ✅ | ||
import foo from 'foo'; | ||
import anotherFoo from 'foo'; | ||
|
||
function bar(foo) { | ||
doSomeThing(anotherFoo, foo); | ||
} | ||
|
||
// ✅ | ||
import foo from 'foo'; | ||
|
||
const anotherFoo = foo; | ||
|
||
function bar(foo) { | ||
doSomeThing(anotherFoo, foo); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import {isCommaToken, isOpeningBraceToken} from '@eslint-community/eslint-utils'; | ||
|
||
export default function * removeSpecifier(specifier, fixer, sourceCode, keepDeclaration = false) { | ||
const declaration = specifier.parent; | ||
const {specifiers} = declaration; | ||
|
||
if (specifiers.length === 1 && !keepDeclaration) { | ||
yield fixer.remove(declaration); | ||
return; | ||
} | ||
|
||
switch (specifier.type) { | ||
case 'ImportSpecifier': { | ||
const isTheOnlyNamedImport = specifiers.every(node => specifier === node || specifier.type !== node.type); | ||
if (isTheOnlyNamedImport) { | ||
const fromToken = sourceCode.getTokenAfter(specifier, token => token.type === 'Identifier' && token.value === 'from'); | ||
|
||
const hasDefaultImport = specifiers.some(node => node.type === 'ImportDefaultSpecifier'); | ||
const startToken = sourceCode.getTokenBefore(specifier, hasDefaultImport ? isCommaToken : isOpeningBraceToken); | ||
const tokenBefore = sourceCode.getTokenBefore(startToken); | ||
|
||
yield fixer.replaceTextRange( | ||
[startToken.range[0], fromToken.range[0]], | ||
tokenBefore.range[1] === startToken.range[0] ? ' ' : '', | ||
); | ||
return; | ||
} | ||
// Fallthrough | ||
} | ||
|
||
case 'ExportSpecifier': | ||
case 'ImportNamespaceSpecifier': | ||
case 'ImportDefaultSpecifier': { | ||
yield fixer.remove(specifier); | ||
|
||
const tokenAfter = sourceCode.getTokenAfter(specifier); | ||
if (isCommaToken(tokenAfter)) { | ||
yield fixer.remove(tokenAfter); | ||
} | ||
|
||
break; | ||
} | ||
|
||
// No default | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import {removeSpecifier} from './fix/index.js'; | ||
import assertToken from './utils/assert-token.js'; | ||
|
||
const MESSAGE_ID = 'no-named-default'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Prefer using the default {{type}} over named {{type}}.', | ||
}; | ||
|
||
const isValueImport = node => !node.importKind || node.importKind === 'value'; | ||
const isValueExport = node => !node.exportKind || node.exportKind === 'value'; | ||
|
||
const fixImportSpecifier = (importSpecifier, {sourceCode}) => function * (fixer) { | ||
const declaration = importSpecifier.parent; | ||
|
||
yield * removeSpecifier(importSpecifier, fixer, sourceCode, /* keepDeclaration */ true); | ||
|
||
const nameText = sourceCode.getText(importSpecifier.local); | ||
const hasDefaultImport = declaration.specifiers.some(({type}) => type === 'ImportDefaultSpecifier'); | ||
|
||
// Insert a new `ImportDeclaration` | ||
if (hasDefaultImport) { | ||
const fromToken = sourceCode.getTokenBefore(declaration.source, token => token.type === 'Identifier' && token.value === 'from'); | ||
const text = `import ${nameText} ${sourceCode.text.slice(fromToken.range[0], declaration.range[1])}`; | ||
yield fixer.insertTextBefore(declaration, `${text}\n`); | ||
|
||
return; | ||
} | ||
|
||
const importToken = sourceCode.getFirstToken(declaration); | ||
assertToken(importToken, { | ||
expected: {type: 'Keyword', value: 'import'}, | ||
ruleId: 'no-named-default', | ||
}); | ||
|
||
const shouldAddComma = declaration.specifiers.some(specifier => specifier !== importSpecifier && specifier.type === importSpecifier.type); | ||
yield fixer.insertTextAfter(importToken, ` ${nameText}${shouldAddComma ? ',' : ''}`); | ||
}; | ||
|
||
const fixExportSpecifier = (exportSpecifier, {sourceCode}) => function * (fixer) { | ||
const declaration = exportSpecifier.parent; | ||
yield * removeSpecifier(exportSpecifier, fixer, sourceCode); | ||
|
||
const text = `export default ${sourceCode.getText(exportSpecifier.local)};`; | ||
yield fixer.insertTextBefore(declaration, `${text}\n`); | ||
}; | ||
|
||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => ({ | ||
ImportSpecifier(specifier) { | ||
if (!( | ||
isValueImport(specifier) | ||
&& specifier.imported.name === 'default' | ||
&& isValueImport(specifier.parent) | ||
)) { | ||
return; | ||
} | ||
|
||
return { | ||
node: specifier, | ||
messageId: MESSAGE_ID, | ||
data: {type: 'import'}, | ||
fix: fixImportSpecifier(specifier, context), | ||
}; | ||
}, | ||
ExportSpecifier(specifier) { | ||
if (!( | ||
isValueExport(specifier) | ||
&& specifier.exported.name === 'default' | ||
&& isValueExport(specifier.parent) | ||
&& !specifier.parent.source | ||
)) { | ||
return; | ||
} | ||
|
||
return { | ||
node: specifier, | ||
messageId: MESSAGE_ID, | ||
data: {type: 'export'}, | ||
fix: fixExportSpecifier(specifier, context), | ||
}; | ||
}, | ||
}); | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
const config = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Disallow named usage of default import and export.', | ||
recommended: true, | ||
}, | ||
fixable: 'code', | ||
messages, | ||
}, | ||
}; | ||
|
||
export default config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import outdent from 'outdent'; | ||
import {getTester, parsers} from './utils/test.js'; | ||
|
||
const {test} = getTester(import.meta); | ||
|
||
// Imports | ||
test.snapshot({ | ||
valid: [ | ||
'import named from "foo";', | ||
'import "foo";', | ||
'import * as named from "foo";', | ||
...[ | ||
'import type {default as named} from "foo";', | ||
'import {type default as named} from "foo";', | ||
].map(code => ({code, languageOptions: {parser: parsers.typescript}})), | ||
], | ||
invalid: [ | ||
'import {default as named} from "foo";', | ||
'import {default as named,} from "foo";', | ||
'import {default as named, bar} from "foo";', | ||
'import {default as named, bar,} from "foo";', | ||
'import defaultExport, {default as named} from "foo";', | ||
'import defaultExport, {default as named,} from "foo";', | ||
'import defaultExport, {default as named, bar} from "foo";', | ||
'import defaultExport, {default as named, bar,} from "foo";', | ||
'import{default as named}from"foo";', | ||
'import {default as named}from"foo";', | ||
'import{default as named} from"foo";', | ||
'import{default as named,}from"foo";', | ||
'import/*comment*/{default as named}from"foo";', | ||
'import /*comment*/{default as named}from"foo";', | ||
'import{default as named}/*comment*/from"foo";', | ||
'import defaultExport,{default as named}from "foo";', | ||
'import defaultExport, {default as named} from "foo" with {type: "json"};', | ||
'import defaultExport, {default as named} from "foo" with {type: "json"}', | ||
'import {default as named1, default as named2,} from "foo";', | ||
], | ||
}); | ||
|
||
// Exports | ||
test.snapshot({ | ||
valid: [ | ||
'export {foo as default} from "foo";', | ||
'export * as default from "foo";', | ||
...[ | ||
'export type {foo as default};', | ||
'export {type foo as default};', | ||
].map(code => ({code, languageOptions: {parser: parsers.typescript}})), | ||
], | ||
invalid: [ | ||
...[ | ||
'export {foo as default};', | ||
'export {foo as default,};', | ||
'export {foo as default, bar};', | ||
'export {foo as default, bar,};', | ||
'export{foo as default};', | ||
].map(code => outdent` | ||
const foo = 1, bar = 2; | ||
${code} | ||
`), | ||
// Invalid, but typescript allow | ||
...[ | ||
'export{foo as default, bar as default};', | ||
outdent` | ||
export default foo; | ||
export {foo as default}; | ||
`, | ||
outdent` | ||
export default bar; | ||
export {foo as default}; | ||
`, | ||
].map(code => ({ | ||
code, | ||
languageOptions: {parser: parsers.typescript}, | ||
})), | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.