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
New: Adds prefer-object-spread rule (refs: #7230) #9955
New: Adds prefer-object-spread rule (refs: #7230) #9955
Conversation
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.
Thank you for the contribution!!
Looks nice to me, but I left some requests.
About 7f7f97d, I'm not sure if it should be a part of this rule, but I don't oppose it.
lib/rules/prefer-object-spread.js
Outdated
) { | ||
context.report({ | ||
node, | ||
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.
Would you use messageId
instead message
?
{ | ||
code: "Object.assign(foo, { bar: baz })", | ||
parserOptions | ||
} |
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.
Would you add tests for ecmaVersion: 2018
?
lib/rules/prefer-object-spread.js
Outdated
|
||
create: function rule(context) { | ||
return { | ||
CallExpression: node => { |
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.
Would you use a consistent way for create
and CallExpression
(e.g., method shorthand)?
I wonder if we have enabled
object-shorthand
rule in our codebase. 🤔
lib/rules/prefer-object-spread.js
Outdated
docs: { | ||
description: | ||
"disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead.", | ||
category: "ECMAScript 6", |
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.
Would you change the category to Stylistic Issues
?
ES6 doesn't look proper category. Though it has not done, #7991 has been accepted.
type: "CallExpression" | ||
} | ||
] | ||
} |
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.
Would you add tests for comments and parentheses? I expect comments to be kept.
For example:
Object.assign(
{},
// a comment
foo,
// another comment
{ a: true }
)
Object.assign(
{},
({ a }),
b ? c : { d },
(e, f)
)
* @returns {Function} autofixer - replaces the Object.assign with a spread object. | ||
*/ | ||
function autofixSpread(node, sourceCode) { | ||
return fixer => { |
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 guess this might be able to simplify with generator function. (It's just a impression)
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 give me an example of this? I'm not too familiar with generator functions.
docs/rules/prefer-object-spread.md
Outdated
|
||
When Object.assign is called using an object literal the first argument, this rule requires using the object spread syntax instead. This rule also warns on cases where an `Object.assign` call is made using a single argument that is an object literal, in this case, the `Object.assign` call is not needed. | ||
|
||
**Please note:** This rule can only be used when using an `ecmaVersion` of 2018 or higher, 9 or higher, or when using an `ecmaVersion` of 2015-2017 or 5-8 with the `experimentalObjectRestSpread` parser option enabled. |
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 that the note about experimentalObjectRestSpread
doesn't need since the option would be removed in nearly future.
Thanks for the comments @mysticatea! I'll get to these tomorrow or Saturday at the latest, just fyi. |
Hi @sharmilajesupaul, just wanted to follow up and ask if this is ready for another round of review or if there is more to be done here. Thanks! |
@platinumazure yes yes wow I completely forgot, sorry! I spent a bunch of time addressing multiline objects that warn on this rule and never updated my PR, I will update it today |
Made a big change to the fixer to work with multiline objects. Now it spreads object literal arguments instead of spreading them. Also addressed a majority of last code review requests. I haven't worked with generators much, so I'm not sure how that will work for the fixer? cc. @ljharb @platinumazure @mysticatea |
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.
Requesting some small documentation changes. Haven't had time to review the code/tests yet, but I hope to do so in the next few days. Thanks!
docs/rules/prefer-object-spread.md
Outdated
@@ -0,0 +1,47 @@ | |||
# Prefer use of an object spread over `Object.assign` (prefer-object-spread) | |||
|
|||
When Object.assign is called using an object literal the first argument, this rule requires using the object spread syntax instead. This rule also warns on cases where an `Object.assign` call is made using a single argument that is an object literal, in this case, the `Object.assign` call is not needed. |
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.
Small typo: "using an object literal the first argument" --> "using an object literal as the first argument".
Also, we generally prefer that the first section should only have background about object spread and Object.assign
-- the Rule Details section is where the rule's purpose/method should be outlined.
docs/rules/prefer-object-spread.md
Outdated
|
||
## Rule Details | ||
|
||
The following patterns are considered errors: |
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 this be replaced with:
Examples of **incorrect** code for this rule:
Thanks!
docs/rules/prefer-object-spread.md
Outdated
Object.assign({ foo: bar }); | ||
``` | ||
|
||
The following patterns are not errors: |
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 this be replaced with:
Examples of **correct** code for this rule:
Thanks!
This was an exception to this rule that we use internally, in the case that an `Object.assign` call is made with an object literal as the only argument. The `Object.assign` call is not needed and we can just use the object literal directly.
- handles nested object literals - handles various comment types - places comma after arguments in a smarter way
97c0127
to
3819ad4
Compare
Just pushed updates to this PR:
Sorry it took so long! |
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 - all the spacing seems reasonable and adjustable via other rules, and using parens wherever it's not known to be safe (because the object literal might be a block) seems the right call.
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.
Finally had time to review this in full (thanks for stopping by the Gitter chat). This is looking great on the whole, love the test cases (including coverage on comments, even HTML comments).
I left a few minor questions and nitpicks. I'm fairly confident at least one or two of them are non-contentious so I've labeled this review as Request Changes.
Thanks for all your hard work (and patience!) on this.
docs/rules/prefer-object-spread.md
Outdated
// Any Object.assign call without an object literal as the first argument | ||
Object.assign(foo, { bar: baz }); | ||
|
||
Object.assign(foo, Object.assign({ bar: 'foo' })); |
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.
Question: Should the inner (second) Object.assign
be flagged here? If so, would it be better to add a non-literal first argument so the whole line is a correct example?
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.
yeah it should good catch
lib/rules/prefer-object-spread.js
Outdated
*/ | ||
function addComma(arg) { | ||
const nonWhitespaceCharacterRegex = /[^\s\\]/g; | ||
const commentRegex = /(\/\*[\w'\s\r\n*]*\*\/)|(\/\/[\w\s']*)|(<![-\-\s\w>/]*>)/g; |
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.
Rather than taking a string and matching for comments, would it make more sense to use token/comment retrieval methods in SourceCode
? I think this basically boils down to getting the last non-comment token in the argument node, and adding a comma afterward.
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.
Thank you for mentioning this. Instead of the regex, we can just use the ranges in context.getCommentsInside(node)
. Definitely over complicated this a bit 😬
lib/rules/prefer-object-spread.js
Outdated
const nonWhitespaceCharacters = Array.from(matchAll(arg, nonWhitespaceCharacterRegex)); | ||
const commentRanges = []; | ||
|
||
// Create a ranges of starting and ending indicies for comments found |
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.
Nit: s/indicies/indices
lib/rules/prefer-object-spread.js
Outdated
create: function rule(context) { | ||
return { | ||
CallExpression(node) { | ||
const sourceCode = context.getSourceCode(); |
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.
Nit: You could move this a few lines up so that we don't need to retrieve the SourceCode
object on every CallExpression. (It's a persistent reference.)
"let a = Object.assign(a, b)", | ||
"Object.assign(a, b)", | ||
"let a = Object.assign(b, { c: 1 })", | ||
"let a = Object.assign({}, ...b)", |
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.
Should this be valid? Wondering if the correct code should be let a = Object.assign(...b);
, or am I missing something? (Compare with the test case 2 lines below this one)
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.
Statement and expression positions differ in terms of how the autofixer outputs parens, but good call, this one should be a spread.
] | ||
}, | ||
|
||
// TODO: Handle nested Object.assign calls |
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.
Should this comment be removed? (Although, now I see some of the test cases below, seems there might be more to do here?)
errors: [ | ||
{ | ||
messageId: "useSpreadMessage", | ||
type: "CallExpression" |
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 this error (and the one below) have more information to help identify where the errors are reported? (e.g., line/column)
{ | ||
code: | ||
"Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, Object.assign({}, { superNested: 'butwhy' })))", | ||
output: "({foo: 'bar', ...Object.assign({ bar: 'foo' }, Object.assign({}, { superNested: 'butwhy' }))})", |
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 innermost Object.assign
should be identified and fixed-- is that a known limitation at this point? Or are we relying on multipass autofix to catch these cases? (Now the TODO above makes more sense...)
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.
Multipass, i think.
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 had a solution earlier that would recurse through nested object.assign calls, but the current implementation relies on multipass
errors: [ | ||
{ | ||
messageId: "useSpreadMessage", | ||
type: "CallExpression" |
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 this error (and the ones below) have more information, e.g. line/column numbers?
- adds line and column numbers to tests - warn on cases where argument is a spread element, - use getCommentsInside instead of regex - fix example in doc
97bab19
to
7a7960a
Compare
@platinumazure ptal, made improvments in 7a7960a |
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.
Left one comment inline.
In addition, I'm not sure about the Object.assign({ objLiteral: "value" })
case. There's no check (that I can see) which checks that this is actually Object.assign. Could you please add a test foo({ bar: "baz" })
and show that this is not reported?
Also, I'm not sure how the case where Object
is overwritten is handled: const Object = {}; Object.assign({ foo: "bar" });
(ideally should not report). I don't think this should block the release of this feature for now, though-- we can fix later.
lib/rules/prefer-object-spread.js
Outdated
schema: [], | ||
fixable: "code", | ||
messages: { | ||
useSpreadMessage: "Use an object spread instead of `Object.assign()` eg: `{ ...foo }`", |
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 don't think the example should be necessary. Users can view the documentation (or search the Internet) for what object spread means.
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's much more helpful to get the warning in-editor, though :-/ is there any reason the example can't be included?
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.
Well, turning that around, why doesn't the object literal case have an example?
Also: Since we have autofix here, the user could just let that be run and see how the item should look. (And no, we don't need to note that --fix
could be run in the lint message, because we note that in most formatters outside of the lint 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.
I suppose that's a fair point - maybe it should have an example :-)
(also since autofix can't be easily run for a single rule, that's not really a practical option for demonstrating the results of a single rule)
…arn. Bug fix to ensure that an we're warning on an `Object.assign` for the literal case.
@platinumazure there should have been a line in the object literal case, to make sure that it was only warning on I also updated the rule to pick up on whether the native |
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.
Sorry about the late review, I just had one question about whether it's necessary to add a dependency.
*/ | ||
function addComma(formattedArg, comments) { | ||
const nonWhitespaceCharacterRegex = /[^\s\\]/g; | ||
const nonWhitespaceCharacters = Array.from(matchAll(formattedArg, nonWhitespaceCharacterRegex)); |
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.
Is it necessary to use matchAll
here? Since this regex doesn't have any capturing groups, it seems like this could just be done with:
const nonWhitespaceCharacters = formattedArg.match(nonWhitespaceCharacterRegex);
That would avoid the need to add a dependency on string.prototype.matchall
.
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.
With a global regex, .match
does not return an array of match objects, so there's no way to get the index (position) of each match - this is the entire reason .matchAll
is needed in the language.
So, no, I don't think there's any way to avoid the dependency (without inlining it, of course, which would be a terrible idea)
What is the purpose of this pull request? (put an "X" next to item)
What changes did you make? (Give an overview)
This is a follow up to #7230, we implemented
prefer-object-spread
internally at @airbnb.The rule requires that you use an object spread over an
Object.assign
call with multiple arguments and an object literal as the first argument.We also warn on cases where an
Object.assign
call is made using a single argument that is an object literal, in this case, theObject.assign
is not needed (7f7f97d).The following patterns are considered errors:
The following patterns are not errors:
Is there anything you'd like reviewers to focus on?
I made 2 commits in this PR
Object.assign
is called using an object literal and an additional argument, which can be changed to use object spread.Object.assign
has a single argument that is an object literal. In this case, you can use the object literal directly. (i.e.Object.assign({})
->{}
)I left the second part in its own commit because I wasn't sure if it would fit into the scope of this rule.
cc. @ljharb