Skip to content
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 getProperties autofixer to no-get rule #1823

Merged
merged 3 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
81 changes: 79 additions & 2 deletions lib/rules/no-get.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,74 @@ function fixGet({
return fixer.replaceText(node, `${objectTextSafe}${objectPathSeparator}${replacementPath}`);
}

function reportGetProperties({ context, node, objectText, properties }) {
let _properties = properties;
if (properties[0].type === 'ArrayExpression') {
// When properties are in an array(e.g. getProperties(this.obj, [bar, foo]) ), actual properties are under Array.elements.
_properties = properties[0].elements;
}
const propertyNames = _properties.map((arg) => arg.value);

context.report({
node,
message: ERROR_MESSAGE_GET_PROPERTIES,
fix(fixer) {
return fixGetProperties({
fixer,
node,
objectText,
propertyNames,
});
},
});
}

function fixGetProperties({ fixer, node, objectText, propertyNames }) {
if (!propertyNames.every((name) => isValidJSVariableName(name) || isValidJSArrayIndex(name))) {
ArtixZ marked this conversation as resolved.
Show resolved Hide resolved
// Do not autofix if any property is invalid.
return null;
ArtixZ marked this conversation as resolved.
Show resolved Hide resolved
}

if (node.parent.type === 'VariableDeclarator' && node.parent.id.type === 'ObjectPattern') {
// When destructuring assignment is in the left side of "=".
// Example: const { foo, bar } = getProperties(this.obj, "foo", "bar");
// Expectation:
// const foo = this.obj.foo;
// const bar = this.obj.bar;

if (node.parent.parent.type !== 'VariableDeclaration' && !node.parent.parent.kind) {
// It has to start with a declaration text. e.g. "const", "let"
return null;
ArtixZ marked this conversation as resolved.
Show resolved Hide resolved
}
const declarationText = node.parent.parent.kind;

const destructuredPropertiesWithAliases = node.parent.id.properties.reduce((acc, property) => {
if (property?.key?.name) {
acc[property.key.name] = property.value.name; // eslint-disable-line no-param-reassign
}
return acc;
}, {});

if (Object.keys(destructuredPropertiesWithAliases).length !== propertyNames.length) {
// When a spread operator in the left side of an assignment. e.g. const { a, b, ...rest } = obj.getProperties(a, b, c, d)
return null;
}

const leadingIndent = ' '.repeat(node.parent.parent.loc.start.column);
const newDeclarations = Object.keys(destructuredPropertiesWithAliases)
.map(
(name) =>
`${declarationText} ${destructuredPropertiesWithAliases[name]} = ${objectText}.${name}`
)
.join(`;\n${leadingIndent}`);

return fixer.replaceTextRange([node.parent.parent.range[0], node.range[1]], newDeclarations);
}

const newProperties = propertyNames.map((name) => `${name}: ${objectText}.${name}`).join(', ');
return fixer.replaceText(node, `{ ${newProperties} }`);
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
ERROR_MESSAGE_GET,
Expand Down Expand Up @@ -269,7 +337,9 @@ module.exports = {
validateGetPropertiesArguments(node.arguments, ignoreNestedPaths)
) {
// Example: this.getProperties('foo', 'bar');
context.report({ node, message: ERROR_MESSAGE_GET_PROPERTIES });
const objectText = context.getSourceCode().getText(node.callee.object);
const properties = node.arguments;
reportGetProperties({ context, node, objectText, properties });
}

if (
Expand All @@ -279,7 +349,14 @@ module.exports = {
validateGetPropertiesArguments(node.arguments.slice(1), ignoreNestedPaths)
) {
// Example: getProperties(this, 'foo', 'bar');
context.report({ node, message: ERROR_MESSAGE_GET_PROPERTIES });
const objectText = context.getSourceCode().getText(node.arguments[0]);
const properties = node.arguments.slice(1);
reportGetProperties({
context,
node,
objectText,
properties,
});
}
},
};
Expand Down
99 changes: 86 additions & 13 deletions tests/lib/rules/no-get.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,45 +321,118 @@ ruleTester.run('no-get', rule, {
// **************************

{
code: "this.getProperties('prop1', 'prop2');",
output: null,
code: "let obj = this.getProperties('prop1', 'prop2');",
output: 'let obj = { prop1: this.prop1, prop2: this.prop2 };',
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: "foo.getProperties('prop1', 'prop2');",
output: null,
code: "const baz = foo.getProperties('prop1', 'prop2');",
output: 'const baz = { prop1: foo.prop1, prop2: foo.prop2 };',
options: [{ catchUnsafeObjects: true }],
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: "this.getProperties(['prop1', 'prop2']);", // With parameters in array.
output: null,
code: "const obj = this.getProperties(['prop1', 'prop2']);", // With parameters in array.
output: 'const obj = { prop1: this.prop1, prop2: this.prop2 };',
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: `
import { getProperties } from '@ember/object';
import { somethingElse } from '@ember/object';
import { random } from 'random';
getProperties(this, 'prop1', 'prop2');
const obj = getProperties(this, 'prop1', 'prop2');
`,
output: `
import { getProperties } from '@ember/object';
import { somethingElse } from '@ember/object';
import { random } from 'random';
const obj = { prop1: this.prop1, prop2: this.prop2 };
`,
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: `
import { getProperties } from '@ember/object';
import { somethingElse } from '@ember/object';
import { random } from 'random';
function foo(){
return getProperties(this, 'prop1', 'prop2');
}
`,
output: `
import { getProperties } from '@ember/object';
import { somethingElse } from '@ember/object';
import { random } from 'random';
function foo(){
return { prop1: this.prop1, prop2: this.prop2 };
}
`,
output: null,
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
// Calling the imported function on an unknown object (without `this`).
code: "import { getProperties } from '@ember/object'; getProperties(foo, 'prop1', 'prop2');",
output: null,
code: "import { getProperties } from '@ember/object'; let obj = getProperties(foo, 'prop1', 'prop2');",
output:
"import { getProperties } from '@ember/object'; let obj = { prop1: foo.prop1, prop2: foo.prop2 };",
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
// With renamed import:
code: "import { getProperties as gp } from '@ember/object'; gp(this, 'prop1', 'prop2');",
output: null,
code: "import { getProperties as gp } from '@ember/object'; const obj = gp(this, 'prop1', 'prop2');",
output:
"import { getProperties as gp } from '@ember/object'; const obj = { prop1: this.prop1, prop2: this.prop2 };",
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: "import { getProperties } from '@ember/object'; getProperties(this, ['prop1', 'prop2']);", // With parameters in array.
code: "import { getProperties } from '@ember/object'; const obj = getProperties(this, ['prop1', 'prop2']);", // With parameters in array.
output:
"import { getProperties } from '@ember/object'; const obj = { prop1: this.prop1, prop2: this.prop2 };",
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: `
import { getProperties } from '@ember/object';
const { foo, bar } = getProperties(
this.obj,
"foo",
"bar"
);
`,
output: `
import { getProperties } from '@ember/object';
const foo = this.obj.foo;
const bar = this.obj.bar;
`,
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: `
import { getProperties } from '@ember/object';
const { foo: qux, bar } = getProperties(
this.obj,
"bar",
"foo"
);
`,
output: `
import { getProperties } from '@ember/object';
const qux = this.obj.foo;
const bar = this.obj.bar;
`,
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
{
code: `
import { getProperties } from '@ember/object';
const { foo, bar, ...qux } = getProperties(
this.obj,
"foo",
"bar",
"baz",
"frex"
);
`,
output: null,
errors: [{ message: ERROR_MESSAGE_GET_PROPERTIES, type: 'CallExpression' }],
},
Expand Down