Skip to content

Commit 2cc7954

Browse files
feat: add restrictDefaultExports option to no-restricted-exports rule (#16785)
* feat: add `restrictDefaultExports` option to no-restricted-exports rule * feat: add restrictDefaultExports.defaultFrom * feat: add restrictDefaultExports.defaultFrom * feat: add restrictDefaultExports.namedFrom * feat: add restrictDefaultExports.namedFrom * feat: add restrictDefaultExports.namespaceFrom * docs: add `restrictDefaultExports` option * docs: fix lint isuues * docs: update sentence * fix: update rule schema * fix: update rule schema * fix: use astUtils.getModuleExportName * refactor: apply suggestions * refactor: update the error message Co-authored-by: Milos Djermanovic <[email protected]> --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent ed60afd commit 2cc7954

3 files changed

Lines changed: 284 additions & 17 deletions

File tree

docs/src/rules/no-restricted-exports.md

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@ By default, this rule doesn't disallow any names. Only the names you specify in
1717
This rule has an object option:
1818

1919
* `"restrictedNamedExports"` is an array of strings, where each string is a name to be restricted.
20+
* `"restrictDefaultExports"` is an object option with boolean properties to restrict certain default export declarations. The option works only if the `restrictedNamedExports` option does not contain the `"default"` value. The following properties are allowed:
21+
* `direct`: restricts `export default` declarations.
22+
* `named`: restricts `export { foo as default };` declarations.
23+
* `defaultFrom`: restricts `export { default } from 'foo';` declarations.
24+
* `namedFrom`: restricts `export { foo as default } from 'foo';` declarations.
25+
* `namespaceFrom`: restricts `export * as default from 'foo';` declarations.
2026

21-
Examples of **incorrect** code for this rule:
27+
### restrictedNamedExports
28+
29+
Examples of **incorrect** code for the `"restrictedNamedExports"` option:
2230

2331
::: incorrect
2432

@@ -50,7 +58,7 @@ export { "👍" } from "some_module";
5058

5159
:::
5260

53-
Examples of **correct** code for this rule:
61+
Examples of **correct** code for the `"restrictedNamedExports"` option:
5462

5563
::: correct
5664

@@ -82,11 +90,11 @@ export { "👍" as thumbsUp } from "some_module";
8290

8391
:::
8492

85-
### Default exports
93+
#### Default exports
8694

87-
By design, this rule doesn't disallow `export default` declarations. If you configure `"default"` as a restricted name, that restriction will apply only to named export declarations.
95+
By design, the `"restrictedNamedExports"` option doesn't disallow `export default` declarations. If you configure `"default"` as a restricted name, that restriction will apply only to named export declarations.
8896

89-
Examples of additional **incorrect** code for this rule:
97+
Examples of additional **incorrect** code for the `"restrictedNamedExports": ["default"]` option:
9098

9199
::: incorrect
92100

@@ -110,7 +118,7 @@ export { default } from "some_module";
110118

111119
:::
112120

113-
Examples of additional **correct** code for this rule:
121+
Examples of additional **correct** code for the `"restrictedNamedExports": ["default"]` option:
114122

115123
::: correct
116124

@@ -122,6 +130,85 @@ export default function foo() {}
122130

123131
:::
124132

133+
### restrictDefaultExports
134+
135+
This option allows you to restrict certain `default` declarations. The option works only if the `restrictedNamedExports` option does not contain the `"default"` value. This option accepts the following properties:
136+
137+
#### direct
138+
139+
Examples of **incorrect** code for the `"restrictDefaultExports": { "direct": true }` option:
140+
141+
::: incorrect
142+
143+
```js
144+
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "direct": true } }]*/
145+
146+
export default foo;
147+
export default 42;
148+
export default function foo() {}
149+
```
150+
151+
:::
152+
153+
#### named
154+
155+
Examples of **incorrect** code for the `"restrictDefaultExports": { "named": true }` option:
156+
157+
::: incorrect
158+
159+
```js
160+
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "named": true } }]*/
161+
162+
const foo = 123;
163+
164+
export { foo as default };
165+
```
166+
167+
:::
168+
169+
#### defaultFrom
170+
171+
Examples of **incorrect** code for the `"restrictDefaultExports": { "defaultFrom": true }` option:
172+
173+
::: incorrect
174+
175+
```js
176+
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "defaultFrom": true } }]*/
177+
178+
export { default } from 'foo';
179+
export { default as default } from 'foo';
180+
```
181+
182+
:::
183+
184+
#### namedFrom
185+
186+
Examples of **incorrect** code for the `"restrictDefaultExports": { "namedFrom": true }` option:
187+
188+
::: incorrect
189+
190+
```js
191+
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "namedFrom": true } }]*/
192+
193+
export { foo as default } from 'foo';
194+
```
195+
196+
:::
197+
198+
#### namespaceFrom
199+
200+
Examples of **incorrect** code for the `"restrictDefaultExports": { "namespaceFrom": true }` option:
201+
202+
::: incorrect
203+
204+
```js
205+
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "namespaceFrom": true } }]*/
206+
207+
export * as default from 'foo';
208+
```
209+
210+
:::
211+
125212
## Known Limitations
126213

127214
This rule doesn't inspect the content of source modules in re-export declarations. In particular, if you are re-exporting everything from another module's export, that export may include a restricted name. This rule cannot detect such cases.

lib/rules/no-restricted-exports.js

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,78 @@ module.exports = {
2727
},
2828

2929
schema: [{
30-
type: "object",
31-
properties: {
32-
restrictedNamedExports: {
33-
type: "array",
34-
items: {
35-
type: "string"
30+
anyOf: [
31+
{
32+
type: "object",
33+
properties: {
34+
restrictedNamedExports: {
35+
type: "array",
36+
items: {
37+
type: "string"
38+
},
39+
uniqueItems: true
40+
}
3641
},
37-
uniqueItems: true
42+
additionalProperties: false
43+
},
44+
{
45+
type: "object",
46+
properties: {
47+
restrictedNamedExports: {
48+
type: "array",
49+
items: {
50+
type: "string",
51+
pattern: "^(?!default$)"
52+
},
53+
uniqueItems: true
54+
},
55+
restrictDefaultExports: {
56+
type: "object",
57+
properties: {
58+
59+
// Allow/Disallow `export default foo; export default 42; export default function foo() {}` format
60+
direct: {
61+
type: "boolean"
62+
},
63+
64+
// Allow/Disallow `export { foo as default };` declarations
65+
named: {
66+
type: "boolean"
67+
},
68+
69+
// Allow/Disallow `export { default } from "mod"; export { default as default } from "mod";` declarations
70+
defaultFrom: {
71+
type: "boolean"
72+
},
73+
74+
// Allow/Disallow `export { foo as default } from "mod";` declarations
75+
namedFrom: {
76+
type: "boolean"
77+
},
78+
79+
// Allow/Disallow `export * as default from "mod"`; declarations
80+
namespaceFrom: {
81+
type: "boolean"
82+
}
83+
},
84+
additionalProperties: false
85+
}
86+
},
87+
additionalProperties: false
3888
}
39-
},
40-
additionalProperties: false
89+
]
4190
}],
4291

4392
messages: {
44-
restrictedNamed: "'{{name}}' is restricted from being used as an exported name."
93+
restrictedNamed: "'{{name}}' is restricted from being used as an exported name.",
94+
restrictedDefault: "Exporting 'default' is restricted."
4595
}
4696
},
4797

4898
create(context) {
4999

50100
const restrictedNames = new Set(context.options[0] && context.options[0].restrictedNamedExports);
101+
const restrictDefaultExports = context.options[0] && context.options[0].restrictDefaultExports;
51102

52103
/**
53104
* Checks and reports given exported name.
@@ -63,6 +114,42 @@ module.exports = {
63114
messageId: "restrictedNamed",
64115
data: { name }
65116
});
117+
return;
118+
}
119+
120+
if (name === "default") {
121+
if (node.parent.type === "ExportAllDeclaration") {
122+
if (restrictDefaultExports && restrictDefaultExports.namespaceFrom) {
123+
context.report({
124+
node,
125+
messageId: "restrictedDefault"
126+
});
127+
}
128+
129+
} else { // ExportSpecifier
130+
const isSourceSpecified = !!node.parent.parent.source;
131+
const specifierLocalName = astUtils.getModuleExportName(node.parent.local);
132+
133+
if (!isSourceSpecified && restrictDefaultExports && restrictDefaultExports.named) {
134+
context.report({
135+
node,
136+
messageId: "restrictedDefault"
137+
});
138+
return;
139+
}
140+
141+
if (isSourceSpecified && restrictDefaultExports) {
142+
if (
143+
(specifierLocalName === "default" && restrictDefaultExports.defaultFrom) ||
144+
(specifierLocalName !== "default" && restrictDefaultExports.namedFrom)
145+
) {
146+
context.report({
147+
node,
148+
messageId: "restrictedDefault"
149+
});
150+
}
151+
}
152+
}
66153
}
67154
}
68155

@@ -73,6 +160,15 @@ module.exports = {
73160
}
74161
},
75162

163+
ExportDefaultDeclaration(node) {
164+
if (restrictDefaultExports && restrictDefaultExports.direct) {
165+
context.report({
166+
node,
167+
messageId: "restrictedDefault"
168+
});
169+
}
170+
},
171+
76172
ExportNamedDeclaration(node) {
77173
const declaration = node.declaration;
78174

tests/lib/rules/no-restricted-exports.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,31 @@ ruleTester.run("no-restricted-exports", rule, {
107107
{ code: "export default 1;", options: [{ restrictedNamedExports: ["default"] }] },
108108

109109
// "default" does not disallow re-exporting a renamed default export from another module
110-
{ code: "export { default as a } from 'foo';", options: [{ restrictedNamedExports: ["default"] }] }
110+
{ code: "export { default as a } from 'foo';", options: [{ restrictedNamedExports: ["default"] }] },
111+
112+
// restrictDefaultExports.direct option
113+
{ code: "export default foo;", options: [{ restrictDefaultExports: { direct: false } }] },
114+
{ code: "export default 42;", options: [{ restrictDefaultExports: { direct: false } }] },
115+
{ code: "export default function foo() {}", options: [{ restrictDefaultExports: { direct: false } }] },
116+
117+
// restrictDefaultExports.named option
118+
{ code: "const foo = 123;\nexport { foo as default };", options: [{ restrictDefaultExports: { named: false } }] },
119+
120+
// restrictDefaultExports.defaultFrom option
121+
{ code: "export { default } from 'mod';", options: [{ restrictDefaultExports: { defaultFrom: false } }] },
122+
{ code: "export { default as default } from 'mod';", options: [{ restrictDefaultExports: { defaultFrom: false } }] },
123+
{ code: "export { foo as default } from 'mod';", options: [{ restrictDefaultExports: { defaultFrom: true } }] },
124+
{ code: "export { default } from 'mod';", options: [{ restrictDefaultExports: { named: true, defaultFrom: false } }] },
125+
{ code: "export { 'default' } from 'mod'; ", options: [{ restrictDefaultExports: { defaultFrom: false } }] },
126+
127+
// restrictDefaultExports.namedFrom option
128+
{ code: "export { foo as default } from 'mod';", options: [{ restrictDefaultExports: { namedFrom: false } }] },
129+
{ code: "export { default as default } from 'mod';", options: [{ restrictDefaultExports: { namedFrom: true } }] },
130+
{ code: "export { default as default } from 'mod';", options: [{ restrictDefaultExports: { namedFrom: false } }] },
131+
{ code: "export { 'default' } from 'mod'; ", options: [{ restrictDefaultExports: { defaultFrom: false, namedFrom: true } }] },
132+
133+
// restrictDefaultExports.namespaceFrom option
134+
{ code: "export * as default from 'mod';", options: [{ restrictDefaultExports: { namespaceFrom: false } }] }
111135
],
112136

113137
invalid: [
@@ -519,6 +543,66 @@ ruleTester.run("no-restricted-exports", rule, {
519543
code: "export { default } from 'foo';",
520544
options: [{ restrictedNamedExports: ["default"] }],
521545
errors: [{ messageId: "restrictedNamed", data: { name: "default" }, type: "Identifier", column: 10 }]
546+
},
547+
548+
// restrictDefaultExports.direct option
549+
{
550+
code: "export default foo;",
551+
options: [{ restrictDefaultExports: { direct: true } }],
552+
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
553+
},
554+
{
555+
code: "export default 42;",
556+
options: [{ restrictDefaultExports: { direct: true } }],
557+
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
558+
},
559+
{
560+
code: "export default function foo() {}",
561+
options: [{ restrictDefaultExports: { direct: true } }],
562+
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
563+
},
564+
{
565+
code: "export default foo;",
566+
options: [{ restrictedNamedExports: ["bar"], restrictDefaultExports: { direct: true } }],
567+
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
568+
},
569+
570+
// restrictDefaultExports.named option
571+
{
572+
code: "const foo = 123;\nexport { foo as default };",
573+
options: [{ restrictDefaultExports: { named: true } }],
574+
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 2, column: 17 }]
575+
},
576+
577+
// restrictDefaultExports.defaultFrom option
578+
{
579+
code: "export { default } from 'mod';",
580+
options: [{ restrictDefaultExports: { defaultFrom: true } }],
581+
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 10 }]
582+
},
583+
{
584+
code: "export { default as default } from 'mod';",
585+
options: [{ restrictDefaultExports: { defaultFrom: true } }],
586+
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 21 }]
587+
},
588+
{
589+
code: "export { 'default' } from 'mod';",
590+
options: [{ restrictDefaultExports: { defaultFrom: true } }],
591+
errors: [{ messageId: "restrictedDefault", type: "Literal", line: 1, column: 10 }]
592+
},
593+
594+
// restrictDefaultExports.namedFrom option
595+
{
596+
code: "export { foo as default } from 'mod';",
597+
options: [{ restrictDefaultExports: { namedFrom: true } }],
598+
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 17 }]
599+
},
600+
601+
// restrictDefaultExports.namespaceFrom option
602+
{
603+
code: "export * as default from 'mod';",
604+
options: [{ restrictDefaultExports: { namespaceFrom: true } }],
605+
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 13 }]
522606
}
523607
]
524608
});

0 commit comments

Comments
 (0)