Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add restrictDefaultExports option to no-restricted-exports rule #16785

Merged
merged 14 commits into from Jan 28, 2023
Merged
99 changes: 93 additions & 6 deletions docs/src/rules/no-restricted-exports.md
Expand Up @@ -17,8 +17,16 @@ By default, this rule doesn't disallow any names. Only the names you specify in
This rule has an object option:

* `"restrictedNamedExports"` is an array of strings, where each string is a name to be restricted.
* `"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:
* `direct`: restricts `export default` declarations.
* `named`: restricts `export { foo as default };` declarations.
* `defaultFrom`: restricts `export { default } from 'foo';` declarations.
* `namedFrom`: restricts `export { foo as default } from 'foo';` declarations.
* `namespaceFrom`: restricts `export * as default from 'foo';` declarations.

Examples of **incorrect** code for this rule:
### restrictedNamedExports

Examples of **incorrect** code for the `"restrictedNamedExports"` option:

::: incorrect

Expand Down Expand Up @@ -50,7 +58,7 @@ export { "👍" } from "some_module";

:::

Examples of **correct** code for this rule:
Examples of **correct** code for the `"restrictedNamedExports"` option:

::: correct

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

:::

### Default exports
#### Default exports

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.
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.

Examples of additional **incorrect** code for this rule:
Examples of additional **incorrect** code for the `"restrictedNamedExports": ["default"]` option:

::: incorrect

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

:::

Examples of additional **correct** code for this rule:
Examples of additional **correct** code for the `"restrictedNamedExports": ["default"]` option:

::: correct

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

:::

### restrictDefaultExports

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:

#### direct

Examples of **incorrect** code for the `"restrictDefaultExports": { "direct": true }` option:

::: incorrect

```js
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "direct": true } }]*/

export default foo;
export default 42;
export default function foo() {};
snitin315 marked this conversation as resolved.
Show resolved Hide resolved
```

:::

#### named

Examples of **incorrect** code for the `"restrictDefaultExports": { "named": true }` option:

::: incorrect

```js
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "named": true } }]*/

const foo = 123;

export { foo as default };
```

:::

#### defaultFrom

Examples of **incorrect** code for the `"restrictDefaultExports": { "defaultFrom": true }` option:

::: incorrect

```js
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "defaultFrom": true } }]*/

export { default } from 'foo';
export { default as default } from 'foo';
```

:::

#### namedFrom

Examples of **incorrect** code for the `"restrictDefaultExports": { "namedFrom": true }` option:

::: incorrect

```js
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "namedFrom": true } }]*/

export { foo as default } from 'foo';
```

:::

#### namespaceFrom

Examples of **incorrect** code for the `"restrictDefaultExports": { "namespaceFrom": true }` option:

::: incorrect

```js
/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "namespaceFrom": true } }]*/

export * as default from 'foo';
```

:::

## Known Limitations

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.
Expand Down
116 changes: 106 additions & 10 deletions lib/rules/no-restricted-exports.js
Expand Up @@ -27,27 +27,78 @@ module.exports = {
},

schema: [{
type: "object",
properties: {
restrictedNamedExports: {
type: "array",
items: {
type: "string"
anyOf: [
{
type: "object",
properties: {
restrictedNamedExports: {
type: "array",
items: {
type: "string"
},
uniqueItems: true
}
},
uniqueItems: true
additionalProperties: false
},
{
type: "object",
properties: {
restrictedNamedExports: {
type: "array",
items: {
type: "string",
pattern: "^(?!default$)"
},
uniqueItems: true
},
restrictDefaultExports: {
type: "object",
properties: {

// Allow/Disallow `export default foo; export default 42; export default function foo() {}` format
direct: {
type: "boolean"
},

// Allow/Disallow `export { foo as default };` declarations
named: {
type: "boolean"
},

// Allow/Disallow `export { default } from "mod"; export { default as default } from "mod";` declarations
defaultFrom: {
type: "boolean"
},

// Allow/Disallow `export { foo as default } from "mod";` declarations
namedFrom: {
type: "boolean"
},

// Allow/Disallow `export * as default from "mod"`; declarations
namespaceFrom: {
type: "boolean"
}
},
additionalProperties: false
}
},
additionalProperties: false
}
},
additionalProperties: false
]
}],

messages: {
restrictedNamed: "'{{name}}' is restricted from being used as an exported name."
restrictedNamed: "'{{name}}' is restricted from being used as an exported name.",
restrictedDefault: "Exporting the 'default' value is restricted."
snitin315 marked this conversation as resolved.
Show resolved Hide resolved
}
},

create(context) {

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

/**
* Checks and reports given exported name.
Expand All @@ -63,6 +114,42 @@ module.exports = {
messageId: "restrictedNamed",
data: { name }
});
return;
}

if (name === "default") {
if (node.parent.type === "ExportAllDeclaration") {
if (restrictDefaultExports && restrictDefaultExports.namespaceFrom) {
context.report({
node,
messageId: "restrictedDefault"
});
}

} else { // ExportSpecifier
const isSourceSpecified = !!node.parent.parent.source;
const specifierLocalName = astUtils.getModuleExportName(node.parent.local);

if (!isSourceSpecified && restrictDefaultExports && restrictDefaultExports.named) {
context.report({
node,
messageId: "restrictedDefault"
});
return;
}

if (isSourceSpecified && restrictDefaultExports) {
if (
(specifierLocalName === "default" && restrictDefaultExports.defaultFrom) ||
(specifierLocalName !== "default" && restrictDefaultExports.namedFrom)
) {
context.report({
node,
messageId: "restrictedDefault"
});
}
}
}
}
}

Expand All @@ -73,6 +160,15 @@ module.exports = {
}
},

ExportDefaultDeclaration(node) {
if (restrictDefaultExports && restrictDefaultExports.direct) {
context.report({
node,
messageId: "restrictedDefault"
});
}
},

ExportNamedDeclaration(node) {
const declaration = node.declaration;

Expand Down
86 changes: 85 additions & 1 deletion tests/lib/rules/no-restricted-exports.js
Expand Up @@ -107,7 +107,31 @@ ruleTester.run("no-restricted-exports", rule, {
{ code: "export default 1;", options: [{ restrictedNamedExports: ["default"] }] },

// "default" does not disallow re-exporting a renamed default export from another module
{ code: "export { default as a } from 'foo';", options: [{ restrictedNamedExports: ["default"] }] }
{ code: "export { default as a } from 'foo';", options: [{ restrictedNamedExports: ["default"] }] },

// restrictDefaultExports.direct option
{ code: "export default foo;", options: [{ restrictDefaultExports: { direct: false } }] },
{ code: "export default 42;", options: [{ restrictDefaultExports: { direct: false } }] },
{ code: "export default function foo() {}", options: [{ restrictDefaultExports: { direct: false } }] },

// restrictDefaultExports.named option
{ code: "const foo = 123;\nexport { foo as default };", options: [{ restrictDefaultExports: { named: false } }] },

// restrictDefaultExports.defaultFrom option
{ code: "export { default } from 'mod';", options: [{ restrictDefaultExports: { defaultFrom: false } }] },
{ code: "export { default as default } from 'mod';", options: [{ restrictDefaultExports: { defaultFrom: false } }] },
{ code: "export { foo as default } from 'mod';", options: [{ restrictDefaultExports: { defaultFrom: true } }] },
{ code: "export { default } from 'mod';", options: [{ restrictDefaultExports: { named: true, defaultFrom: false } }] },
{ code: "export { 'default' } from 'mod'; ", options: [{ restrictDefaultExports: { defaultFrom: false } }] },

// restrictDefaultExports.namedFrom option
{ code: "export { foo as default } from 'mod';", options: [{ restrictDefaultExports: { namedFrom: false } }] },
{ code: "export { default as default } from 'mod';", options: [{ restrictDefaultExports: { namedFrom: true } }] },
{ code: "export { default as default } from 'mod';", options: [{ restrictDefaultExports: { namedFrom: false } }] },
{ code: "export { 'default' } from 'mod'; ", options: [{ restrictDefaultExports: { defaultFrom: false, namedFrom: true } }] },

// restrictDefaultExports.namespaceFrom option
{ code: "export * as default from 'mod';", options: [{ restrictDefaultExports: { namespaceFrom: false } }] }
],

invalid: [
Expand Down Expand Up @@ -519,6 +543,66 @@ ruleTester.run("no-restricted-exports", rule, {
code: "export { default } from 'foo';",
options: [{ restrictedNamedExports: ["default"] }],
errors: [{ messageId: "restrictedNamed", data: { name: "default" }, type: "Identifier", column: 10 }]
},

// restrictDefaultExports.direct option
{
code: "export default foo;",
options: [{ restrictDefaultExports: { direct: true } }],
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
},
{
code: "export default 42;",
options: [{ restrictDefaultExports: { direct: true } }],
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
},
{
code: "export default function foo() {};",
snitin315 marked this conversation as resolved.
Show resolved Hide resolved
options: [{ restrictDefaultExports: { direct: true } }],
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
},
{
code: "export default foo;",
options: [{ restrictedNamedExports: ["bar"], restrictDefaultExports: { direct: true } }],
errors: [{ messageId: "restrictedDefault", type: "ExportDefaultDeclaration", column: 1 }]
},

// restrictDefaultExports.named option
{
code: "const foo = 123;\nexport { foo as default };",
options: [{ restrictDefaultExports: { named: true } }],
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 2, column: 17 }]
},

// restrictDefaultExports.defaultFrom option
{
code: "export { default } from 'mod';",
options: [{ restrictDefaultExports: { defaultFrom: true } }],
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 10 }]
},
{
code: "export { default as default } from 'mod';",
options: [{ restrictDefaultExports: { defaultFrom: true } }],
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 21 }]
},
{
code: "export { 'default' } from 'mod';",
options: [{ restrictDefaultExports: { defaultFrom: true } }],
errors: [{ messageId: "restrictedDefault", type: "Literal", line: 1, column: 10 }]
},

// restrictDefaultExports.namedFrom option
{
code: "export { foo as default } from 'mod';",
options: [{ restrictDefaultExports: { namedFrom: true } }],
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 17 }]
},

// restrictDefaultExports.namespaceFrom option
{
code: "export * as default from 'mod';",
options: [{ restrictDefaultExports: { namespaceFrom: true } }],
errors: [{ messageId: "restrictedDefault", type: "Identifier", line: 1, column: 13 }]
}
]
});