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 new rule no-assignment-of-untracked-properties-used-in-tracking-contexts #855

Merged
merged 1 commit into from
Jun 25, 2020
Merged

Add new rule no-assignment-of-untracked-properties-used-in-tracking-contexts #855

merged 1 commit into from
Jun 25, 2020

Conversation

bmish
Copy link
Member

@bmish bmish commented Jun 16, 2020

See rule documentation file for explanation and examples.

CC: @mongoose700
CC: @pzuraq who added this assertion originally

@bmish bmish requested a review from rwjblue June 16, 2020 18:27

if (types.isClassDeclaration(nodeClass)) {
// Native class.
nodeClass.body.body.forEach((node) => {
Copy link
Collaborator

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(...).

Copy link
Member Author

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) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This could use filters and a map.

Copy link
Member Author

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();
Copy link
Collaborator

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.

Copy link
Member Author

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) =>
Copy link
Collaborator

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.

Copy link
Member Author

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') {
Copy link
Collaborator

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.

Copy link
Member Author

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})`);
Copy link
Collaborator

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?

Copy link
Member Author

@bmish bmish Jun 18, 2020

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

@@ -223,6 +224,19 @@ function isEmberCoreModule(context, node, moduleName) {
return false;
}

function isAnyEmberModule(context, node) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this include Helper?

Copy link
Member Author

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; }
Copy link
Collaborator

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)) {
Copy link
Collaborator

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.

Copy link
Member Author

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.')) {
Copy link
Collaborator

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@@ -635,6 +635,29 @@ describe('isEmberObjectProxy', () => {
});
});

describe('isEmberHelper', () => {
describe("should check if it's an Ember helper", () => {
it('should detect when using native classes', () => {
Copy link
Collaborator

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?

Copy link
Member Author

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');
Copy link
Member

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.

Copy link
Member

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).

Copy link
Member Author

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) {
Copy link
Member

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';
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky...

Copy link
Member

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.

Copy link
Member Author

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;
Copy link
Member

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?

Copy link
Member Author

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.

@bmish
Copy link
Member Author

bmish commented Jun 18, 2020

@rwjblue I'm planning to rename this rule to no-assignment-of-untracked-properties-used-in-tracking-contexts to be more accurate and to allow it to be expanded to handle other tracking contexts beyond just computed properties (such as observers) in the future. Let me know if you have any thoughts.

@rwjblue
Copy link
Member

rwjblue commented Jun 18, 2020

Ya, that sounds good to me. In theory, this could apply to any {{this.foo}} in a template too (and I honestly think we could make that work).

@bmish bmish changed the title Add new rule no-assignment-of-computed-property-dependencies Add new rule no-assignment-of-untracked-properties-used-in-tracking-contexts Jun 18, 2020
* @param {Node} nodeClass - Node for the Ember class
* @returns {String[]} - list of dependent keys used inside the class
*/
function findComputedPropertyDependentKeys(nodeClass, computedImportName) {
Copy link
Collaborator

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.

Copy link
Member Author

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);
Copy link
Collaborator

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?

Copy link
Member Author

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);
Copy link
Collaborator

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')?

Copy link
Member Author

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.

@bmish bmish requested a review from rwjblue June 22, 2020 16:26
push(item) {
this.stack.push(item);
}
peak() {
Copy link
Collaborator

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.

Copy link
Member Author

@bmish bmish Jun 22, 2020

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,
Copy link
Collaborator

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.

Copy link
Member Author

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();
Copy link
Collaborator

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})`);
Copy link
Collaborator

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?

Copy link
Member Author

@bmish bmish Jun 22, 2020

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).

Copy link
Collaborator

@mongoose700 mongoose700 Jun 22, 2020

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?

Copy link
Member Author

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) ||
Copy link
Collaborator

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?

Copy link
Member Author

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';`
Copy link
Collaborator

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?

Copy link
Member Author

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.

@bmish
Copy link
Member Author

bmish commented Jun 23, 2020

Expecting to merge this later today. Thanks for the great reviews.

@bmish bmish merged commit 82acc5b into ember-cli:master Jun 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants