-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add no-anonymous-default-export
rule
#712
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 class
es, 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use > -1
here, !~
is a bit too clever ;-)
* @author Duncan Beevers | ||
*/ | ||
|
||
module.exports = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a native speaker, but doesn't of an anonymous function
sound better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - it should handle arrays, but template literals i'd expect to be covered under "literals".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Options are now handled as a single object. Details are spelled out in the rule description file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var -> const
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
], | ||
|
||
invalid: [ | ||
test({ code: 'export default []', errors: [{ message: 'Unexpected default export of an array' }] }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a test with a string and one with a template literal ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def.option
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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