Add no-anonymous-default-export rule#712
Conversation
3b25d5b to
6a47f2d
Compare
6a47f2d to
949ac38
Compare
|
Should this then warn if you default-export an arrow function? what about an anonymous Why are literals forbidden? The identifier name of their import would usually match the filename, by convention. |
|
The idea is to improve grepability of the codebase. When grepping around for an identifier, filenames aren't matched on, only the contents of the file. I agree that it should warn if you default-export an arrow function or an anonymous |
949ac38 to
72d4d31
Compare
|
@ljharb Now forbids default exports of anonymous class expressions and arrow functions. Added functionality, specs and documentation to cover these cases. |
c7efe8e to
74af3e3
Compare
ljharb
left a comment
There was a problem hiding this comment.
Currently, this checks for primitive literals, object literals, anon classes, anon functions, and arrows. What would you think about an option for each of these items?
My personal use cases would only enable the functions and classes, but object literals in particular I'd need to be able to turn off.
74af3e3 to
5a8d6ef
Compare
|
@ljharb Each forbidden unnamed default export type can now be selectively allowed with an option. |
ljharb
left a comment
There was a problem hiding this comment.
Looking much better, thanks!
Couple comments, and could we add at least one example or note with multiple options passed?
|
|
||
| create: function (context) { | ||
|
|
||
| const forbid = (option) => !~context.options.indexOf('allow-' + option) |
There was a problem hiding this comment.
let's use > -1 here, !~ is a bit too clever ;-)
| * @author Duncan Beevers | ||
| */ | ||
|
|
||
| module.exports = { |
There was a problem hiding this comment.
please add a schema to the rule, so that invalid options are rejected.
5a8d6ef to
7c07aa0
Compare
7c07aa0 to
818bee2
Compare
|
babb7cd to
b1dafb3
Compare
jfmengels
left a comment
There was a problem hiding this comment.
Hi @duncanbeevers! Thanks for the interesting rule and making a PR for it :)
I have written some ways this could be simplified, feel free to disagree if you feel I'm mistaken somewhere.
| // Allow forbidden types with multiple options | ||
| test({ code: 'export default 123', options: ['allow-literal', 'allow-object-expression'] }), | ||
| test({ code: 'export default {}', options: ['allow-literal', 'allow-object-expression'] }), | ||
|
|
There was a problem hiding this comment.
Could you add few valid tests for other kind of exports (named exports, export * from 'foo').
They are properly ignored at the moment, but it's mostly for documentation's sake.
(Might just be me, but I like to add additional tests slightly out of bounds of what is being treated to prevent surprises)
There was a problem hiding this comment.
Added a few specs for a couple of flavors of exports and labeled the section
// Sanity check unrelated export syntaxes|
|
||
| ruleTester.run('no-anonymous-default-export', rule, { | ||
| valid: [ | ||
| // Named exports are valid |
There was a problem hiding this comment.
This is a confusing comment, as named exports are a thing of their own
export const foo = 2;Instead, "not anonymous", "with a name"? Or whatever you feel like naming it.
There was a problem hiding this comment.
Updated this comment to
// Exports with identifiers are valid| const type = node.declaration.type | ||
| const noID = !node.declaration.id | ||
|
|
||
| if (type === 'Literal' && forbid(MAP[type])) { |
There was a problem hiding this comment.
You can simplify the conditions by having this pretty much at the start of the function:
if (!forbid(MAP[type])) {
return;
}| if (type === 'FunctionDeclaration' && noID && forbid(MAP[type])) { | ||
| context.report({ | ||
| node: node, | ||
| message: 'Unexpected default export of anonymous function', |
There was a problem hiding this comment.
I'm not a native speaker, but doesn't of an anonymous function sound better?
There was a problem hiding this comment.
I've added articles are all the default export types for consistency.
a literal
an array
an object
etc...
| return | ||
| } | ||
|
|
||
| if (type === 'ObjectExpression' && forbid(MAP[type])) { |
There was a problem hiding this comment.
I think you can make all this less repetitive:
const types = { // or whatever you wish to name it
ObjectExpression: {
validation: () => true,
description: 'object expression',
},
FunctionDeclaration: {
validation: id => !id,
description: 'anonymous function',
},
// ...
}
// ....
'ExportDefaultDeclaration': (node) => {
if (forbid(MAP[type]) && types[type] && types[type].validation(node.declaration.id)) {
context.report({
node: node,
message: 'Unexpected default export of ' + types[type].description,
})
}
}There was a problem hiding this comment.
The architecture of the rule now roughly matches this suggested layout.
| @@ -0,0 +1,51 @@ | |||
| # no-anonymous-default-export | |||
|
|
|||
| Reports if a module's default export is unnamed. This includes several types of unnamed data types; literals, object expressions, anonymous functions, arrow functions, and anonymous class declarations. | |||
There was a problem hiding this comment.
I would also enforce this on arrays, which I for instance sometimes use to create a fixtures file.
Also, I think it would make sense to also handle template literals if we intend to handle pretty much any value ?
There was a problem hiding this comment.
Agreed - it should handle arrays, but template literals i'd expect to be covered under "literals".
There was a problem hiding this comment.
Yes, I was thinking the same thing, no real reason to handle them separately from normal strings and numbers.
There was a problem hiding this comment.
Added enforcement for arrays, and a spec for the handling of template literals.
| module.exports = { | ||
| meta: { | ||
| schema: { | ||
| type: 'array', |
There was a problem hiding this comment.
I think the options should better be passed as an object, with a true/false values to each key. I found that having one object with settings instead of multiple values in a given order makes it much more extendable.
My gut feeling is to make it so that a field set to true would make it forbidden, and false would make it ignored, and that you'd have it true by default.
/*eslint import/no-anonymous-default-export: [2, "allow-arrow-function-expression"]*/
-->
/*eslint import/no-anonymous-default-export: [2, {ArrowFunctionExpression: false}]*/The schema would look something like this (IIRC) (adapted from what is done in https://github.com/benmosher/eslint-plugin-import/blob/master/src/rules/no-extraneous-dependencies.js)
schema: [
{
'type': 'object',
'properties': {
'Literal': { 'type': 'boolean' },
'ObjectExpression': { 'type': 'boolean' },
'FunctionDeclaration': { 'type': 'boolean' },
'ArrowFunctionExpression': { 'type': 'boolean' },
'ClassDeclaration': { 'type': 'boolean' },
},
'additionalProperties': false,
},
],And you'd get options like
create: function (context) {
const options = context.options[0] || {}
}There was a problem hiding this comment.
I would be fine with simplifying the name of the properties (like ArrowFunctionExpression --> ArrowFunction), you'd just need to account for the difference in the code.
There was a problem hiding this comment.
Options are now handled as a single object. Details are spelled out in the rule description file.
There was a problem hiding this comment.
The names are explicit, I like it 👍
b1dafb3 to
2a12e67
Compare
| import { RuleTester } from 'eslint' | ||
|
|
||
| var ruleTester = new RuleTester() | ||
| var rule = require('rules/no-anonymous-default-export') |
| ], | ||
|
|
||
| invalid: [ | ||
| test({ code: 'export default []', errors: [{ message: 'Unexpected default export of an array' }] }), |
There was a problem hiding this comment.
I think the error messages are confusing.
Unexpected default export of an anonymous class/function are fine, because they mention that what is wrong is the anonymousness.
For the others, it feels like this rule says that arrays/literals/objects are not allowed to be the default export for some reason.
I think it would be clearer if we recommended giving it a name or assigning it to a variable.
There was a problem hiding this comment.
The error messages and schema description strings have all been made explicit. The messages about anonymous functions and classes remain the same, while the rest of the messages are updated to use a more active voice.
'Assign literal to a variable before exporting as module default'
| test({ code: 'export default []', errors: [{ message: 'Unexpected default export of an array' }] }), | ||
| test({ code: 'export default () => {}', errors: [{ message: 'Unexpected default export of an arrow function' }] }), | ||
| test({ code: 'export default class {}', errors: [{ message: 'Unexpected default export of an anonymous class' }] }), | ||
| test({ code: 'export default 123', errors: [{ message: 'Unexpected default export of a literal' }] }), |
There was a problem hiding this comment.
Could you add a test with a string and one with a template literal ?
There was a problem hiding this comment.
I added tests with string and template literal cases to both the success and failure corpuses, and found that template literals were not yet forbidden.
I corrected this by adding a TemplateLiteral clause to the defs structure whose value is intentionally identical to Literal's
| map((key) => defs[key]). | ||
| reduce((acc, def) => { | ||
| acc[def.option] = { | ||
| description: 'If `false`, will not report default export of ' + def.message, |
There was a problem hiding this comment.
This sounds like the opposite of what the name suggests.
From what I understand, if allowLiteral is set to true, then Literals will be reported. 🤔
This should be reversed and the default value should be false.
There was a problem hiding this comment.
Yes, this fell out of date as I reworked the implementation. Corrected.
| const defaultOptions = Object.keys(defs). | ||
| map((key) => defs[key]). | ||
| reduce((acc, def) => { | ||
| acc[def.options] = false |
There was a problem hiding this comment.
This defaultOptions object doesn't strictly need to be constructed. I created it (poorly) for parity with the documentation. Since it doesn't affect behavior, I'm rewinding my decision here and removing this.
2a12e67 to
ad9e908
Compare
jfmengels
left a comment
There was a problem hiding this comment.
LGTM, thanks for all your work @duncanbeevers! :)
I'm letting the others some time to have a final look, and will merge soon if they don't.
3 similar comments
ad9e908 to
2a9a7a1
Compare
|
I added an example case to the documentation of exporting an identified rather than anonymous class. |
|
@benmosher Let me know if there's anything I can do to shepherd this along. |
|
Thanks a lot for this @duncanbeevers! (and sorry for the delay in merging this) |
Forbids exporting anonymous functions, literals, class expressions, or arrow functions as module default.
This is intended to improve the grepability of codebases by encouraging the re-use of identifiers at their declaration site and at their import sites.
Rule Details
Fail
Pass