-
-
Notifications
You must be signed in to change notification settings - Fork 204
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
Do not autofix self-referential computed properties in require-computed-macros
rule
#1121
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,6 +115,17 @@ function getThisExpressions(nodeLogicalExpression) { | |
return arrayOfThisExpressions.reverse(); | ||
} | ||
|
||
function detectCircularKey(node, macroArgs) { | ||
if (node.parent && node.parent.key) { | ||
// `class { foo = computed() }` and `EmberObject({ foo: computed() })` usage | ||
return macroArgs.includes(node.parent.key.name); | ||
} else if (types.isMethodDefinition(node) && node.key.name) { | ||
// class { @computed() get foo() } | ||
return macroArgs.includes(node.key.name); | ||
} | ||
return false; | ||
} | ||
|
||
function makeFix(nodeToReplace, macro, macroArgs) { | ||
const isDecoratorUsage = types.isMethodDefinition(nodeToReplace); | ||
const prefix = isDecoratorUsage ? '@' : ''; // Decorator usage has @ symbol prefixing computed() | ||
|
@@ -161,71 +172,116 @@ module.exports = { | |
|
||
create(context) { | ||
function reportSingleArg(nodeComputedProperty, nodeWithThisExpression, macro) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const text = propertyGetterUtils.nodeToDependentKey(nodeWithThisExpression, context); | ||
return fixer.replaceText( | ||
nodeComputedProperty, | ||
makeFix(nodeComputedProperty, macro, `'${text}'`) | ||
); | ||
}, | ||
}); | ||
const text = propertyGetterUtils.nodeToDependentKey(nodeWithThisExpression, context); | ||
const isNotFixable = detectCircularKey(nodeComputedProperty, [text]); | ||
|
||
if (isNotFixable) { | ||
context.report({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of duplicating the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bmish I did that initially but it is linted against, I also suspect it's not good practice since it might cause eslint to report the violation as fixable when it is not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not linted against, we actually have some examples of autofixers with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was the lint rule it triggered https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
}); | ||
} else { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const text = propertyGetterUtils.nodeToDependentKey(nodeWithThisExpression, context); | ||
const fix = makeFix(nodeComputedProperty, macro, `'${text}'`); | ||
|
||
return fixer.replaceText(nodeComputedProperty, fix); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
function reportBinaryExpression(nodeComputedProperty, nodeBinaryExpression, macro) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const textLeft = propertyGetterUtils.nodeToDependentKey( | ||
nodeBinaryExpression.left, | ||
context | ||
); | ||
const textRight = sourceCode.getText(nodeBinaryExpression.right); | ||
return fixer.replaceText( | ||
nodeComputedProperty, | ||
makeFix(nodeComputedProperty, macro, `'${textLeft}', ${textRight}`) | ||
); | ||
}, | ||
}); | ||
const sourceCode = context.getSourceCode(); | ||
const argLeft = propertyGetterUtils.nodeToDependentKey(nodeBinaryExpression.left, context); | ||
const argRight = sourceCode.getText(nodeBinaryExpression.right); | ||
const isNotFixable = detectCircularKey(nodeComputedProperty, [argLeft, argRight]); | ||
|
||
if (isNotFixable) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
}); | ||
} else { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const textLeft = propertyGetterUtils.nodeToDependentKey( | ||
nodeBinaryExpression.left, | ||
context | ||
); | ||
const textRight = sourceCode.getText(nodeBinaryExpression.right); | ||
const fix = makeFix(nodeComputedProperty, macro, `'${textLeft}', ${textRight}`); | ||
|
||
return fixer.replaceText(nodeComputedProperty, fix); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
function reportLogicalExpression(nodeComputedProperty, nodeLogicalExpression, macro) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const text = getThisExpressions(nodeLogicalExpression) | ||
.map((node) => propertyGetterUtils.nodeToDependentKey(node, context)) | ||
.join("', '"); | ||
return fixer.replaceText( | ||
nodeComputedProperty, | ||
makeFix(nodeComputedProperty, macro, `'${text}'`) | ||
); | ||
}, | ||
}); | ||
const compArgs = getThisExpressions(nodeLogicalExpression).map((node) => | ||
propertyGetterUtils.nodeToDependentKey(node, context) | ||
); | ||
const isNotFixable = detectCircularKey(nodeComputedProperty, compArgs); | ||
|
||
if (isNotFixable) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
}); | ||
} else { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const text = getThisExpressions(nodeLogicalExpression) | ||
.map((node) => propertyGetterUtils.nodeToDependentKey(node, context)) | ||
.join("', '"); | ||
const fix = makeFix(nodeComputedProperty, macro, `'${text}'`); | ||
|
||
return fixer.replaceText(nodeComputedProperty, fix); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
function reportFunctionCall(nodeComputedProperty, nodeCallExpression, macro) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const arg1 = propertyGetterUtils.nodeToDependentKey( | ||
nodeCallExpression.callee.object, | ||
context | ||
); | ||
const restOfArgs = nodeCallExpression.arguments.map((arg) => sourceCode.getText(arg)); | ||
return fixer.replaceText( | ||
nodeComputedProperty, | ||
makeFix(nodeComputedProperty, macro, `'${arg1}', ${restOfArgs.join(', ')}`) | ||
); | ||
}, | ||
}); | ||
const sourceCode = context.getSourceCode(); | ||
const arg1 = propertyGetterUtils.nodeToDependentKey( | ||
nodeCallExpression.callee.object, | ||
context | ||
); | ||
const restOfArgs = nodeCallExpression.arguments.map((arg) => sourceCode.getText(arg)); | ||
const isNotFixable = detectCircularKey(nodeComputedProperty, [arg1, ...restOfArgs]); | ||
|
||
if (isNotFixable) { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
}); | ||
} else { | ||
context.report({ | ||
node: nodeComputedProperty, | ||
message: makeErrorMessage(macro), | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const arg1 = propertyGetterUtils.nodeToDependentKey( | ||
nodeCallExpression.callee.object, | ||
context | ||
); | ||
const restOfArgs = nodeCallExpression.arguments.map((arg) => sourceCode.getText(arg)); | ||
const fix = makeFix(nodeComputedProperty, macro, `'${arg1}', ${restOfArgs.join(', ')}`); | ||
|
||
return fixer.replaceText(nodeComputedProperty, fix); | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
let importedEmberName; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -287,5 +287,56 @@ ruleTester.run('require-computed-macros', rule, { | |
output: "class Test { @computed.mapBy('children', 'age') someProp }", | ||
errors: [{ message: ERROR_MESSAGE_MAP_BY, type: 'MethodDefinition' }], | ||
}, | ||
|
||
// Self Referential | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're adding a few redundant test cases as well as missing some test cases. We need the following test cases basic test cases which test a simple single-arg return expression that will be converted into the
And we need to test other types of expressions that will be converted into other macros:
We don't need to test anything related to the dependent key arguments ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The redundancy is intentional, it covers both forms of classic and both forms of class based declarations both with and without args being supplied to the computed as dependent keys (since I encountered errors from conversion from both in the wild). Can add tests for a few of the others. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. A comment next to each test case explaining what variation it is testing would help. |
||
{ | ||
code: 'class Test { @computed() get foo() { return this.foo; } }', | ||
options: [{ includeNativeGetters: true }], | ||
output: 'class Test { @computed() get foo() { return this.foo; } }', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't that imply the rule was deleting code? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. It means there's no autofix. And there's actually a lint rule to enforce this but unfortunately it's not working on this code because the array of test cases has |
||
errors: [{ message: ERROR_MESSAGE_READS, type: 'MethodDefinition' }], | ||
}, | ||
{ | ||
code: "class Test { @computed('foo') get foo() { return this.foo; } }", | ||
options: [{ includeNativeGetters: true }], | ||
output: "class Test { @computed('foo') get foo() { return this.foo; } }", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'MethodDefinition' }], | ||
}, | ||
{ | ||
code: "class Test { @computed('foo', 'bar') get foo() { return this.foo; } }", | ||
options: [{ includeNativeGetters: true }], | ||
output: "class Test { @computed('foo', 'bar') get foo() { return this.foo; } }", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'MethodDefinition' }], | ||
}, | ||
{ | ||
code: "class Test { foo = computed('foo', function() { return this.foo; }) }", | ||
output: "class Test { foo = computed('foo', function() { return this.foo; }) }", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }], | ||
}, | ||
{ | ||
code: "class Test { foo = computed('foo', 'bar', function() { return this.foo; }) }", | ||
output: "class Test { foo = computed('foo', 'bar', function() { return this.foo; }) }", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }], | ||
}, | ||
{ | ||
code: | ||
"import EmberObject from '@ember/object'; const Test = EmberObject.extend({ foo: computed('foo', function() { return this.foo; }) })", | ||
output: | ||
"import EmberObject from '@ember/object'; const Test = EmberObject.extend({ foo: computed('foo', function() { return this.foo; }) })", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }], | ||
}, | ||
{ | ||
code: | ||
"import EmberObject from '@ember/object'; const Test = EmberObject.extend({ foo: computed('foo', 'bar', function() { return this.foo; }) })", | ||
output: | ||
"import EmberObject from '@ember/object'; const Test = EmberObject.extend({ foo: computed('foo', 'bar', function() { return this.foo; }) })", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }], | ||
}, | ||
{ | ||
code: | ||
"import EmberObject from '@ember/object'; const Test = EmberObject.extend({ foo: computed(function() { return this.foo; }) })", | ||
output: | ||
"import EmberObject from '@ember/object'; const Test = EmberObject.extend({ foo: computed(function() { return this.foo; }) })", | ||
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }], | ||
}, | ||
].map(addComputedImport), | ||
}); |
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.
There should always be a parent, but we do need to check what node type the parent is before we check its
key
. That way, we won't end up using thekey
of some unexpected kind of 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.
oddly I believe I encountered a no parent situation in our own test suite. Will double check that.
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.
ESLint adds the
parent
when it runs the rule test cases. But if you write separate unit tests for this function by itself, then it won't have a parent. So feel free to add the parent guard but you only need if you write separate unit tests for the function itself.