Skip to content

Commit

Permalink
feat: add restrictDefaultExports option to no-restricted-exports ru…
Browse files Browse the repository at this point in the history
…le (#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 <milos.djermanovic@gmail.com>

---------

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
snitin315 and mdjermanovic committed Jan 28, 2023
1 parent ed60afd commit 2cc7954
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 17 deletions.
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() {}
```

:::

#### 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 'default' is restricted."
}
},

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() {}",
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 }]
}
]
});

0 comments on commit 2cc7954

Please sign in to comment.