forked from eslint/eslint
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New: Adds prefer-object-spread rule (refs: eslint#7230)
- Loading branch information
1 parent
d64fbb4
commit 3a26cd3
Showing
4 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# 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. | ||
|
||
**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. | ||
|
||
## Rule Details | ||
|
||
The following patterns are considered errors: | ||
|
||
```js | ||
|
||
Object.assign({}, foo) | ||
|
||
Object.assign({}, {foo: 'bar'}) | ||
|
||
Object.assign({ foo: 'bar'}, baz) | ||
|
||
Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' })) | ||
|
||
Object.assign({}, { foo, bar, baz }) | ||
|
||
Object.assign({}, { ...baz }) | ||
|
||
``` | ||
|
||
The following patterns are not errors: | ||
|
||
```js | ||
|
||
Object.assign(...foo); | ||
|
||
// Any Object.assign call without an object literal as the first argument | ||
Object.assign(foo, { bar: baz }); | ||
|
||
Object.assign(foo, Object.assign({ bar: 'foo' })); | ||
|
||
Object.assign(foo, { bar, baz }) | ||
|
||
Object.assign(foo, { ...baz }); | ||
|
||
// Object.assign with a single argument that is an object literal | ||
Object.assign({}); | ||
|
||
Object.assign({ foo: bar }); | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
When you don't care about syntactic sugar added by the object spread property. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/** | ||
* @fileoverview Prefers object spread property over Object.assign | ||
* @author Sharmila Jesupaul | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
|
||
"use strict"; | ||
|
||
// Helpers | ||
/** | ||
* Helper that returns the last element in an array | ||
* @param {array} arr - array that you are searching in | ||
* @returns {string} - Returns the last element in an array of string arguments | ||
*/ | ||
function tail(arr) { | ||
return arr[arr.length - 1]; | ||
} | ||
|
||
/** | ||
* Helper that strips the curlie braces from a stringified object, returning only the contents | ||
* @param {string} objectString - Source code of an object literal | ||
* @returns {string} - Returns the object with the curly braces stripped | ||
*/ | ||
function stripCurlies(objectString) { | ||
return objectString.slice(1, -1); | ||
} | ||
|
||
/** | ||
* Helper that checks if the node is an Object.assign call | ||
* @param {ASTNode} node - The node that the rule warns on | ||
* @returns {boolean} - Returns true if the node is an Object.assign call | ||
*/ | ||
function isObjectAssign(node) { | ||
return ( | ||
node.callee && | ||
node.callee.type === "MemberExpression" && | ||
node.callee.object.name === "Object" && | ||
node.callee.property.name === "assign" | ||
); | ||
} | ||
|
||
/** | ||
* Autofixer that parses arguments that are passed into the Object.assign call, and returns a formatted object spread. | ||
* @param {array} args - The node that the rule warns on, i.e. the Object.assign call | ||
* @param {string} sourceCode - sourceCode of the Object.assign call | ||
* @returns {array} formatted args - replaces the Object.assign with a spread object. | ||
*/ | ||
function parseArgs(args, sourceCode) { | ||
const mapped = args.map((arg, i) => { | ||
|
||
// If the the argument is an empty object | ||
if (arg.type === "ObjectExpression" && arg.properties.length === 0) { | ||
return ""; | ||
} | ||
|
||
// if the argument is another object.assign call run this function for all of it's arguments | ||
if (isObjectAssign(arg)) { | ||
return parseArgs(arg.arguments, sourceCode); | ||
} | ||
|
||
const next = args[i + 1] || {}; | ||
|
||
// if the argument is an object with properties | ||
if (arg.type === "ObjectExpression") { | ||
const trimmedObject = stripCurlies(sourceCode.getText(arg)).trim(); | ||
|
||
return next.range && next.range[0] ? `${trimmedObject}, ` : trimmedObject; | ||
} | ||
|
||
return next.range && next.range[0] | ||
? `...${sourceCode.getText(arg)}, ` | ||
: `...${sourceCode.getText(arg)}`; | ||
}); | ||
|
||
return [].concat.apply([], mapped); | ||
} | ||
|
||
/** | ||
* Autofixes the Object.assign call to use an object spread instead. | ||
* @param {ASTNode|null} node - The node that the rule warns on, i.e. the Object.assign call | ||
* @param {string} sourceCode - sourceCode of the Object.assign call | ||
* @returns {Function} autofixer - replaces the Object.assign with a spread object. | ||
*/ | ||
function autofixSpread(node, sourceCode) { | ||
return fixer => { | ||
const args = node.arguments; | ||
|
||
const processedArgs = parseArgs(args, sourceCode).join(""); | ||
|
||
const lastArg = tail(args); | ||
const firstArg = args[0]; | ||
|
||
const funcEnd = sourceCode.text | ||
.slice(lastArg.range[1], node.range[1]) | ||
.split(")")[0]; | ||
const funcStart = tail( | ||
sourceCode.text.slice(node.range[0], firstArg.range[0]).split("(") | ||
); | ||
|
||
return fixer.replaceText( | ||
node, | ||
`{${funcStart}${processedArgs}${funcEnd}}` | ||
); | ||
}; | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
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", | ||
recommended: false, | ||
url: "https://eslint.org/docs/rules/prefer-object-spread" | ||
}, | ||
schema: [], | ||
fixable: "code" | ||
}, | ||
|
||
create: function rule(context) { | ||
return { | ||
CallExpression: node => { | ||
const message = "Use an object spread instead of `Object.assign()` eg: `{ ...foo }`"; | ||
const sourceCode = context.getSourceCode(); | ||
const hasSpreadElement = node.arguments.length && | ||
node.arguments.some(x => x.type === "SpreadElement"); | ||
|
||
if ( | ||
node.arguments.length > 1 && | ||
node.arguments[0].type === "ObjectExpression" && | ||
isObjectAssign && | ||
!hasSpreadElement | ||
) { | ||
context.report({ | ||
node, | ||
message, | ||
fix: autofixSpread(node, sourceCode) | ||
}); | ||
} | ||
} | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
/** | ||
* @fileoverview Prefers object spread property over Object.assign | ||
* @author Sharmila Jesupaul | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
|
||
"use strict"; | ||
|
||
const rule = require("../../../lib/rules/prefer-object-spread"); | ||
|
||
const RuleTester = require("../../../lib/testers/rule-tester"); | ||
|
||
const parserOptions = { | ||
ecmaVersion: 6, | ||
ecmaFeatures: { | ||
experimentalObjectRestSpread: true | ||
} | ||
}; | ||
|
||
const defaultErrorMessage = | ||
"Use an object spread instead of `Object.assign()` eg: `{ ...foo }`"; | ||
const ruleTester = new RuleTester(); | ||
|
||
ruleTester.run("prefer-object-spread", rule, { | ||
valid: [ | ||
{ | ||
code: "const bar = { ...foo }", | ||
parserOptions | ||
}, | ||
{ | ||
code: "Object.assign(...foo)", | ||
parserOptions | ||
}, | ||
{ | ||
code: "Object.assign(foo, { bar: baz })", | ||
parserOptions | ||
} | ||
], | ||
|
||
invalid: [ | ||
{ | ||
code: "Object.assign({}, foo)", | ||
output: "{...foo}", | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
{ | ||
code: "Object.assign({}, { foo: 'bar' })", | ||
output: "{foo: 'bar'}", | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
{ | ||
code: "Object.assign({}, baz, { foo: 'bar' })", | ||
output: "{...baz, foo: 'bar'}", | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
{ | ||
code: "Object.assign({}, { foo: 'bar', baz: 'foo' })", | ||
output: "{foo: 'bar', baz: 'foo'}", | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
{ | ||
code: "Object.assign({ foo: 'bar' }, baz)", | ||
output: "{foo: 'bar', ...baz}", | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
|
||
// Many args | ||
{ | ||
code: "Object.assign({ foo: 'bar' }, cats, dogs, trees, birds)", | ||
output: "{foo: 'bar', ...cats, ...dogs, ...trees, ...birds}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
|
||
// Nested Object.assign calls | ||
{ | ||
code: | ||
"Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz))", | ||
output: "{foo: 'bar', bar: 'foo', ...baz}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
}, | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
{ | ||
code: | ||
"Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, Object.assign({}, { superNested: 'butwhy' })))", | ||
output: "{foo: 'bar', bar: 'foo', superNested: 'butwhy'}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
}, | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
}, | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
|
||
// Mix spread in argument | ||
{ | ||
code: "Object.assign({ foo: 'bar', ...bar }, baz)", | ||
output: "{foo: 'bar', ...bar, ...baz}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
|
||
// Object shorthand | ||
{ | ||
code: "Object.assign({}, { foo, bar, baz })", | ||
output: "{foo, bar, baz}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
|
||
// Objects with computed properties | ||
{ | ||
code: "Object.assign({}, { [bar]: 'foo' })", | ||
output: "{[bar]: 'foo'}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
}, | ||
|
||
// Objects with spread properties | ||
{ | ||
code: "Object.assign({ ...bar }, { ...baz })", | ||
output: "{...bar, ...baz}", | ||
parserOptions, | ||
errors: [ | ||
{ | ||
message: defaultErrorMessage, | ||
type: "CallExpression" | ||
} | ||
] | ||
} | ||
] | ||
}); |