Skip to content

Commit

Permalink
New: Adds prefer-object-spread rule (refs: eslint#7230)
Browse files Browse the repository at this point in the history
  • Loading branch information
sharmilajesupaul committed Feb 6, 2018
1 parent d64fbb4 commit 3a26cd3
Show file tree
Hide file tree
Showing 4 changed files with 389 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/eslint-recommended.js
Expand Up @@ -229,6 +229,7 @@ module.exports = {
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-numeric-literals": "off",
"prefer-object-spread": "off",
"prefer-promise-reject-errors": "off",
"prefer-reflect": "off",
"prefer-rest-params": "off",
Expand Down
50 changes: 50 additions & 0 deletions docs/rules/prefer-object-spread.md
@@ -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.
143 changes: 143 additions & 0 deletions lib/rules/prefer-object-spread.js
@@ -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)
});
}
}
};
}
};
195 changes: 195 additions & 0 deletions tests/lib/rules/prefer-object-spread.js
@@ -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"
}
]
}
]
});

0 comments on commit 3a26cd3

Please sign in to comment.