Skip to content

Commit ed8da1b

Browse files
fiskersindresorhus
andauthoredJan 24, 2025··
Add no-named-default rule (#2538)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 2985ecc commit ed8da1b

11 files changed

+923
-43
lines changed
 

‎docs/rules/no-named-default.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Disallow named usage of default import and export
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
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.
11+
12+
## Examples
13+
14+
```js
15+
//
16+
import {default as foo} from 'foo';
17+
18+
//
19+
import foo from 'foo';
20+
```
21+
22+
```js
23+
//
24+
import {default as foo, bar} from 'foo';
25+
26+
//
27+
import foo, {bar} from 'foo';
28+
```
29+
30+
```js
31+
//
32+
export {foo as default};
33+
34+
//
35+
export default foo;
36+
```
37+
38+
```js
39+
//
40+
export {foo as default, bar};
41+
42+
//
43+
export default foo;
44+
export {bar};
45+
```
46+
47+
```js
48+
//
49+
import foo, {default as anotherFoo} from 'foo';
50+
51+
function bar(foo) {
52+
doSomeThing(anotherFoo, foo);
53+
}
54+
55+
//
56+
import foo from 'foo';
57+
import anotherFoo from 'foo';
58+
59+
function bar(foo) {
60+
doSomeThing(anotherFoo, foo);
61+
}
62+
63+
//
64+
import foo from 'foo';
65+
66+
const anotherFoo = foo;
67+
68+
function bar(foo) {
69+
doSomeThing(anotherFoo, foo);
70+
}
71+
```

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export default [
9494
| [no-length-as-slice-end](docs/rules/no-length-as-slice-end.md) | Disallow using `.length` as the `end` argument of `{Array,String,TypedArray}#slice()`. || 🔧 | |
9595
| [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. || 🔧 | |
9696
| [no-magic-array-flat-depth](docs/rules/no-magic-array-flat-depth.md) | Disallow a magic number as the `depth` argument in `Array#flat(…).` || | |
97+
| [no-named-default](docs/rules/no-named-default.md) | Disallow named usage of default import and export. || 🔧 | |
9798
| [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. || 🔧 | |
9899
| [no-negation-in-equality-check](docs/rules/no-negation-in-equality-check.md) | Disallow negated expression in equality check. || | 💡 |
99100
| [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. || 🔧 | |

‎rules/fix/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {default as replaceReferenceIdentifier} from './replace-reference-identif
1212
export {default as renameVariable} from './rename-variable.js';
1313
export {default as replaceNodeOrTokenAndSpacesBefore} from './replace-node-or-token-and-spaces-before.js';
1414
export {default as removeSpacesAfter} from './remove-spaces-after.js';
15+
export {default as removeSpecifier} from './remove-specifier.js';
1516
export {default as fixSpaceAroundKeyword} from './fix-space-around-keywords.js';
1617
export {default as replaceStringRaw} from './replace-string-raw.js';
1718
export {default as addParenthesizesToReturnOrThrowExpression} from './add-parenthesizes-to-return-or-throw-expression.js';

‎rules/fix/remove-specifier.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {isCommaToken, isOpeningBraceToken} from '@eslint-community/eslint-utils';
2+
3+
export default function * removeSpecifier(specifier, fixer, sourceCode, keepDeclaration = false) {
4+
const declaration = specifier.parent;
5+
const {specifiers} = declaration;
6+
7+
if (specifiers.length === 1 && !keepDeclaration) {
8+
yield fixer.remove(declaration);
9+
return;
10+
}
11+
12+
switch (specifier.type) {
13+
case 'ImportSpecifier': {
14+
const isTheOnlyNamedImport = specifiers.every(node => specifier === node || specifier.type !== node.type);
15+
if (isTheOnlyNamedImport) {
16+
const fromToken = sourceCode.getTokenAfter(specifier, token => token.type === 'Identifier' && token.value === 'from');
17+
18+
const hasDefaultImport = specifiers.some(node => node.type === 'ImportDefaultSpecifier');
19+
const startToken = sourceCode.getTokenBefore(specifier, hasDefaultImport ? isCommaToken : isOpeningBraceToken);
20+
const tokenBefore = sourceCode.getTokenBefore(startToken);
21+
22+
yield fixer.replaceTextRange(
23+
[startToken.range[0], fromToken.range[0]],
24+
tokenBefore.range[1] === startToken.range[0] ? ' ' : '',
25+
);
26+
return;
27+
}
28+
// Fallthrough
29+
}
30+
31+
case 'ExportSpecifier':
32+
case 'ImportNamespaceSpecifier':
33+
case 'ImportDefaultSpecifier': {
34+
yield fixer.remove(specifier);
35+
36+
const tokenAfter = sourceCode.getTokenAfter(specifier);
37+
if (isCommaToken(tokenAfter)) {
38+
yield fixer.remove(tokenAfter);
39+
}
40+
41+
break;
42+
}
43+
44+
// No default
45+
}
46+
}

‎rules/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import noKeywordPrefix from './no-keyword-prefix.js';
3939
import noLengthAsSliceEnd from './no-length-as-slice-end.js';
4040
import noLonelyIf from './no-lonely-if.js';
4141
import noMagicArrayFlatDepth from './no-magic-array-flat-depth.js';
42+
import noNamedDefault from './no-named-default.js';
4243
import noNegatedCondition from './no-negated-condition.js';
4344
import noNegationInEqualityCheck from './no-negation-in-equality-check.js';
4445
import noNestedTernary from './no-nested-ternary.js';
@@ -166,6 +167,7 @@ const rules = {
166167
'no-length-as-slice-end': createRule(noLengthAsSliceEnd, 'no-length-as-slice-end'),
167168
'no-lonely-if': createRule(noLonelyIf, 'no-lonely-if'),
168169
'no-magic-array-flat-depth': createRule(noMagicArrayFlatDepth, 'no-magic-array-flat-depth'),
170+
'no-named-default': createRule(noNamedDefault, 'no-named-default'),
169171
'no-negated-condition': createRule(noNegatedCondition, 'no-negated-condition'),
170172
'no-negation-in-equality-check': createRule(noNegationInEqualityCheck, 'no-negation-in-equality-check'),
171173
'no-nested-ternary': createRule(noNestedTernary, 'no-nested-ternary'),

‎rules/no-named-default.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {removeSpecifier} from './fix/index.js';
2+
import assertToken from './utils/assert-token.js';
3+
4+
const MESSAGE_ID = 'no-named-default';
5+
const messages = {
6+
[MESSAGE_ID]: 'Prefer using the default {{type}} over named {{type}}.',
7+
};
8+
9+
const isValueImport = node => !node.importKind || node.importKind === 'value';
10+
const isValueExport = node => !node.exportKind || node.exportKind === 'value';
11+
12+
const fixImportSpecifier = (importSpecifier, {sourceCode}) => function * (fixer) {
13+
const declaration = importSpecifier.parent;
14+
15+
yield * removeSpecifier(importSpecifier, fixer, sourceCode, /* keepDeclaration */ true);
16+
17+
const nameText = sourceCode.getText(importSpecifier.local);
18+
const hasDefaultImport = declaration.specifiers.some(({type}) => type === 'ImportDefaultSpecifier');
19+
20+
// Insert a new `ImportDeclaration`
21+
if (hasDefaultImport) {
22+
const fromToken = sourceCode.getTokenBefore(declaration.source, token => token.type === 'Identifier' && token.value === 'from');
23+
const text = `import ${nameText} ${sourceCode.text.slice(fromToken.range[0], declaration.range[1])}`;
24+
yield fixer.insertTextBefore(declaration, `${text}\n`);
25+
26+
return;
27+
}
28+
29+
const importToken = sourceCode.getFirstToken(declaration);
30+
assertToken(importToken, {
31+
expected: {type: 'Keyword', value: 'import'},
32+
ruleId: 'no-named-default',
33+
});
34+
35+
const shouldAddComma = declaration.specifiers.some(specifier => specifier !== importSpecifier && specifier.type === importSpecifier.type);
36+
yield fixer.insertTextAfter(importToken, ` ${nameText}${shouldAddComma ? ',' : ''}`);
37+
};
38+
39+
const fixExportSpecifier = (exportSpecifier, {sourceCode}) => function * (fixer) {
40+
const declaration = exportSpecifier.parent;
41+
yield * removeSpecifier(exportSpecifier, fixer, sourceCode);
42+
43+
const text = `export default ${sourceCode.getText(exportSpecifier.local)};`;
44+
yield fixer.insertTextBefore(declaration, `${text}\n`);
45+
};
46+
47+
/** @param {import('eslint').Rule.RuleContext} context */
48+
const create = context => ({
49+
ImportSpecifier(specifier) {
50+
if (!(
51+
isValueImport(specifier)
52+
&& specifier.imported.name === 'default'
53+
&& isValueImport(specifier.parent)
54+
)) {
55+
return;
56+
}
57+
58+
return {
59+
node: specifier,
60+
messageId: MESSAGE_ID,
61+
data: {type: 'import'},
62+
fix: fixImportSpecifier(specifier, context),
63+
};
64+
},
65+
ExportSpecifier(specifier) {
66+
if (!(
67+
isValueExport(specifier)
68+
&& specifier.exported.name === 'default'
69+
&& isValueExport(specifier.parent)
70+
&& !specifier.parent.source
71+
)) {
72+
return;
73+
}
74+
75+
return {
76+
node: specifier,
77+
messageId: MESSAGE_ID,
78+
data: {type: 'export'},
79+
fix: fixExportSpecifier(specifier, context),
80+
};
81+
},
82+
});
83+
84+
/** @type {import('eslint').Rule.RuleModule} */
85+
const config = {
86+
create,
87+
meta: {
88+
type: 'suggestion',
89+
docs: {
90+
description: 'Disallow named usage of default import and export.',
91+
recommended: true,
92+
},
93+
fixable: 'code',
94+
messages,
95+
},
96+
};
97+
98+
export default config;

‎rules/prefer-export-from.js

+2-43
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {isCommaToken, isOpeningBraceToken, isClosingBraceToken} from '@eslint-community/eslint-utils';
1+
import {isOpeningBraceToken} from '@eslint-community/eslint-utils';
22
import {isStringLiteral} from './ast/index.js';
3+
import {removeSpecifier} from './fix/index.js';
34

45
const MESSAGE_ID_ERROR = 'error';
56
const MESSAGE_ID_SUGGESTION = 'suggestion';
@@ -29,48 +30,6 @@ const isTypeExport = specifier => specifier.exportKind === 'type' || specifier.p
2930

3031
const isTypeImport = specifier => specifier.importKind === 'type' || specifier.parent.importKind === 'type';
3132

32-
function * removeSpecifier(node, fixer, sourceCode) {
33-
const {parent} = node;
34-
const {specifiers} = parent;
35-
36-
if (specifiers.length === 1) {
37-
yield * removeImportOrExport(parent, fixer, sourceCode);
38-
return;
39-
}
40-
41-
switch (node.type) {
42-
case 'ImportSpecifier': {
43-
const hasOtherSpecifiers = specifiers.some(specifier => specifier !== node && specifier.type === node.type);
44-
if (!hasOtherSpecifiers) {
45-
const closingBraceToken = sourceCode.getTokenAfter(node, isClosingBraceToken);
46-
47-
// If there are other specifiers, they have to be the default import specifier
48-
// And the default import has to write before the named import specifiers
49-
// So there must be a comma before
50-
const commaToken = sourceCode.getTokenBefore(node, isCommaToken);
51-
yield fixer.replaceTextRange([commaToken.range[0], closingBraceToken.range[1]], '');
52-
return;
53-
}
54-
// Fallthrough
55-
}
56-
57-
case 'ExportSpecifier':
58-
case 'ImportNamespaceSpecifier':
59-
case 'ImportDefaultSpecifier': {
60-
yield fixer.remove(node);
61-
62-
const tokenAfter = sourceCode.getTokenAfter(node);
63-
if (isCommaToken(tokenAfter)) {
64-
yield fixer.remove(tokenAfter);
65-
}
66-
67-
break;
68-
}
69-
70-
// No default
71-
}
72-
}
73-
7433
function * removeImportOrExport(node, fixer, sourceCode) {
7534
switch (node.type) {
7635
case 'ImportSpecifier':

‎test/no-named-default.js

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import outdent from 'outdent';
2+
import {getTester, parsers} from './utils/test.js';
3+
4+
const {test} = getTester(import.meta);
5+
6+
// Imports
7+
test.snapshot({
8+
valid: [
9+
'import named from "foo";',
10+
'import "foo";',
11+
'import * as named from "foo";',
12+
...[
13+
'import type {default as named} from "foo";',
14+
'import {type default as named} from "foo";',
15+
].map(code => ({code, languageOptions: {parser: parsers.typescript}})),
16+
],
17+
invalid: [
18+
'import {default as named} from "foo";',
19+
'import {default as named,} from "foo";',
20+
'import {default as named, bar} from "foo";',
21+
'import {default as named, bar,} from "foo";',
22+
'import defaultExport, {default as named} from "foo";',
23+
'import defaultExport, {default as named,} from "foo";',
24+
'import defaultExport, {default as named, bar} from "foo";',
25+
'import defaultExport, {default as named, bar,} from "foo";',
26+
'import{default as named}from"foo";',
27+
'import {default as named}from"foo";',
28+
'import{default as named} from"foo";',
29+
'import{default as named,}from"foo";',
30+
'import/*comment*/{default as named}from"foo";',
31+
'import /*comment*/{default as named}from"foo";',
32+
'import{default as named}/*comment*/from"foo";',
33+
'import defaultExport,{default as named}from "foo";',
34+
'import defaultExport, {default as named} from "foo" with {type: "json"};',
35+
'import defaultExport, {default as named} from "foo" with {type: "json"}',
36+
'import {default as named1, default as named2,} from "foo";',
37+
],
38+
});
39+
40+
// Exports
41+
test.snapshot({
42+
valid: [
43+
'export {foo as default} from "foo";',
44+
'export * as default from "foo";',
45+
...[
46+
'export type {foo as default};',
47+
'export {type foo as default};',
48+
].map(code => ({code, languageOptions: {parser: parsers.typescript}})),
49+
],
50+
invalid: [
51+
...[
52+
'export {foo as default};',
53+
'export {foo as default,};',
54+
'export {foo as default, bar};',
55+
'export {foo as default, bar,};',
56+
'export{foo as default};',
57+
].map(code => outdent`
58+
const foo = 1, bar = 2;
59+
${code}
60+
`),
61+
// Invalid, but typescript allow
62+
...[
63+
'export{foo as default, bar as default};',
64+
outdent`
65+
export default foo;
66+
export {foo as default};
67+
`,
68+
outdent`
69+
export default bar;
70+
export {foo as default};
71+
`,
72+
].map(code => ({
73+
code,
74+
languageOptions: {parser: parsers.typescript},
75+
})),
76+
],
77+
});

‎test/package.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([
3434
'consistent-existence-index-check',
3535
'prefer-global-this',
3636
'no-instanceof-builtin-object',
37+
'no-named-default',
3738
'consistent-assert',
3839
'no-accessor-recursion',
3940
]);

‎test/snapshots/no-named-default.js.md

+624
Large diffs are not rendered by default.
1.32 KB
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.