-
-
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
Add new rule no-assignment-of-untracked-properties-used-in-tracking-contexts
#855
Add new rule no-assignment-of-untracked-properties-used-in-tracking-contexts
#855
Conversation
|
||
if (types.isClassDeclaration(nodeClass)) { | ||
// Native class. | ||
nodeClass.body.body.forEach((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.
nit: I would prefer a flatMap
here to forEach
and push(...)
.
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.
Fixed.
function findTrackedProperties(nodeClassDeclaration) { | ||
const results = []; | ||
|
||
nodeClassDeclaration.body.body.forEach((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.
nit: This could use filter
s and a map
.
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.
Fixed.
} | ||
|
||
// State being tracked for this file. | ||
const trackedProperties = new Set(); |
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.
If someone declared multiple classes in the same file, this wouldn't get reset between them.
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.
Fixed as per other comment, and added a test case for this.
currentEmberModule = node; | ||
|
||
// Gather computed property dependent keys from this class. | ||
findComputedPropertyDependentKeys(node).forEach((key) => |
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: I think it would be better to do
let computedPropertyDependentKeys = new Set(findComputedPropertyDependentKeys(node))
instead of using forEach
and add
. It would help with making sure we clean up state from any previous classes.
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.
Much better, fixed.
while (current.object.type === 'MemberExpression') { | ||
current = current.object; | ||
} | ||
if (current.object.type !== 'ThisExpression') { |
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.
Technically we could be in a nested function that wouldn't actually be called with the class object for this
. Though if we tried to account for all cases like that, we wouldn't be able to make a fix for this rule at all, because you could call any method with a different this
if you really wanted to.
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.
Good point, I'm going to ignore that for now.
node, | ||
message: ERROR_MESSAGE, | ||
fix(fixer) { | ||
return fixer.replaceText(node, `this.set('${propertyName}', ${nodeTextRight})`); |
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.
If making it a tracked property is an option, should we do that instead?
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.
That's a much bigger change, and would be much more likely to cause behavior changes. Might be better to save that for either an optional rule option or codemod.
https://github.com/ember-codemods/ember-tracked-properties-codemod
lib/utils/ember.js
Outdated
@@ -223,6 +224,19 @@ function isEmberCoreModule(context, node, moduleName) { | |||
return false; | |||
} | |||
|
|||
function isAnyEmberModule(context, 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.
Should this include Helper
?
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 probably should so I have added helper to this list.
import Component from '@ember/component'; | ||
class MyClass extends Component { | ||
@computed('args.x') get prop() {} | ||
myFunction() { this.args.x = 123; } |
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.
This looks like something you shouldn't be doing anyway :p
}, | ||
|
||
CallExpression(node) { | ||
if (emberUtils.isAnyEmberModule(context, 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.
The tracked properties need to be reset here.
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.
Fixed.
const nodeTextRight = sourceCode.getText(node.right); | ||
const propertyName = nodeTextLeft.replace('this.', ''); | ||
|
||
if (propertyName.startsWith('args.')) { |
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.
We should make sure that we're in a Glimmer component for this check. You could have an args
property that needs to be tracked on a class Ember component, or in a service.
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.
Fixed.
tests/lib/utils/ember-test.js
Outdated
@@ -635,6 +635,29 @@ describe('isEmberObjectProxy', () => { | |||
}); | |||
}); | |||
|
|||
describe('isEmberHelper', () => { | |||
describe("should check if it's an Ember helper", () => { | |||
it('should detect when using native classes', () => { |
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 also test Helper.extends
?
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.
Done.
if (types.isClassDeclaration(nodeClass)) { | ||
// Native JS class. | ||
return javascriptUtils.flatMap(nodeClass.body.body, (node) => { | ||
const computedDecorator = decoratorUtils.findDecorator(node, 'computed'); |
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.
This situation is exactly the same as with observer dependent keys, we should probably account for that in the same rule here.
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.
Also, we really ought to make this check for the imported computed (vs just any method that happens to be named computed
).
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.
Updated to use imported name for computed
and added a test.
|
||
return { | ||
// Native JS class: | ||
ClassDeclaration(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.
We should probably handle enter
/exit
here, and push/pop onto a stack of known tracked properties.
For example, a file with the following contents should still warn:
export default class extends Service {
@computed('foo') whatever() {
// ...snip...
}
otherFunc() {
class InternalStateTracker {
@tracked foo;
}
this.foo = 'lolol';
}
}
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.
This is tricky...
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.
Ya, indeed. I was thinking that the stack thing would make it work.
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.
@rwjblue good news, I switched to a stack to handle nested classes, and added a test case for this. Thank you for the inspiration!
// State being tracked for this file. | ||
let trackedProperties = undefined; | ||
let computedPropertyDependentKeys = undefined; | ||
let currentEmberModule = null; |
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.
This variable name is odd, what does it represent?
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.
The node for the current Ember module (component, controller, etc) that we're inside. Made some slight updates to naming/comments for clarity.
@rwjblue I'm planning to rename this rule to |
Ya, that sounds good to me. In theory, this could apply to any |
no-assignment-of-computed-property-dependencies
no-assignment-of-untracked-properties-used-in-tracking-contexts
* @param {Node} nodeClass - Node for the Ember class | ||
* @returns {String[]} - list of dependent keys used inside the class | ||
*/ | ||
function findComputedPropertyDependentKeys(nodeClass, computedImportName) { |
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 we also find things like computed.readOnly
or @readOnly
? It could also be useful to allow for configuring extra methods to be recognized as creating computed properties, in case people make custom ones.
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.
Good point, I will think about how to support these computed property macros.
|
||
return node.arguments | ||
.filter((arg) => arg.type === 'Literal' && typeof arg.value === 'string') | ||
.map((node) => node.value); |
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.
Do we need to do anything special for @each
here?
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.
Good point, updated to handle that, and added a test case.
|
||
return node.arguments | ||
.filter((arg) => arg.type === 'Literal' && typeof arg.value === 'string') | ||
.map((node) => node.value); |
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.
Does this currently handle @computed('foo.bar')
and this.foo = 'a'
(as opposed to this.foo.bar = 'a'
)?
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.
Good point, updated to handle that, and added a test case.
push(item) { | ||
this.stack.push(item); | ||
} | ||
peak() { |
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.
This should be peek
instead.
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.
Doh! Fixed.
node, | ||
computedPropertyDependentKeys, | ||
trackedProperties, | ||
isInGlimmerComponent, |
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: I think isGlimmerComponent
would be better than isInGlimmerComponent
in this context.
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.
Agreed. I had made this same change locally but hadn't pushed yet.
return; | ||
} | ||
|
||
const currentClass = classStack.peak(); |
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 do
const { isInGlimmerComponent, computedPropertyDependentKeys, trackedProperties } =
node, | ||
message: ERROR_MESSAGE, | ||
fix(fixer) { | ||
return fixer.replaceText(node, `this.set('${propertyName}', ${nodeTextRight})`); |
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.
Won't this.set
be invalid for Glimmer components or non-classic usages of Ember components?
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.
Regarding Glimmer components, I just updated to remove the autofixer, and added a TODO comment saying that we should autofix to use set()
once we are capable of adding the import statement for it.
Regarding native components, I will keep the this.set()
autofixer since that still works, even though it is deprecated (and can be easily fixed just by switching to the imported version).
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.
Do we also need to worry about Ember components using extends Component {
that don't have the classic
decorator?
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 redid the autofixer to always use the imported set
function and add an import statement if needed. So this shouldn't be an issue anymore.
isEmberRoute(context, node) || | ||
isEmberService(context, node) || | ||
isEmberArrayProxy(context, node) || | ||
isEmberObjectProxy(context, 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.
Should EmberObject
be added to this list as well?
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 was thinking about that. I have now added it.
fix(fixer) { | ||
if (currentClass.isGlimmerComponent) { | ||
// Glimmer components should have no autofix since `this.set` is not available in them. | ||
// TODO: autofix to `set()` once we can add the import: `import { set } from '@ember/object';` |
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.
What's the reason we can't do this?
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.
Good news, I went ahead and updated the autofixer to always use the imported set
function, and add an import statement if needed.
Expecting to merge this later today. Thanks for the great reviews. |
See rule documentation file for explanation and examples.
CC: @mongoose700
CC: @pzuraq who added this assertion originally