-
-
Notifications
You must be signed in to change notification settings - Fork 211
[NEW RULE] avoid-leaking-callbacks-in-ember-objects #422
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
base: master
Are you sure you want to change the base?
Changes from 8 commits
8012125
707e9a8
adfa4eb
87bed74
4fb3db2
4cabe4c
3d4cf72
db6d2df
08625d0
eb30a54
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 |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
|
|
||
| # Avoid callback leaks in Ember objects | ||
| ## Rule `avoid-leaking-callbacks-in-ember-objects` | ||
|
|
||
| Callback leaks are memory leaks that occur due to state being caught in a callback function that is never released from memory. | ||
|
|
||
| Since callback functions for things like event listeners and interval timers are retained by reference elsewhere, you must be careful to unregister them when no longer needed or ensure that the context they're registered with is destroyed. | ||
|
|
||
|
|
||
| Examples of **incorrect** code for this rule: | ||
|
|
||
| ```js | ||
| export default Ember.Component.extend({ | ||
| didInsertElement() { | ||
| if (this.get('onScroll')) { | ||
| window.addEventListener('scroll', (...args) => this.get('onScroll')(...args)); | ||
| } | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| Examples of **correct** code for this rule: | ||
|
|
||
| ```js | ||
| export default Ember.Component.extend({ | ||
| didInsertElement() { | ||
| if (this.get('onScroll')) { | ||
| this._onScrollHandler = (...args) => this.get('onScroll')(...args); | ||
| window.addEventListener('scroll', this._onScrollHandler); | ||
| } | ||
| }, | ||
|
|
||
| willDestroy() { | ||
| window.removeEventListener('scroll', this._onScrollHandler); | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| ## Further Reading | ||
|
|
||
| https://github.com/ember-best-practices/memory-leak-examples/blob/master/exercises/exercise-2.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,7 @@ module.exports = { | |
| 'routes-segments-snake-case': require('./rules/routes-segments-snake-case'), | ||
| 'use-brace-expansion': require('./rules/use-brace-expansion'), | ||
| 'use-ember-get-and-set': require('./rules/use-ember-get-and-set'), | ||
| 'avoid-leaking-callbacks-in-ember-objects': require('./rules/avoid-leaking-callbacks-in-ember-objects'), | ||
|
Member
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 should be alphabetical in the list.
Contributor
Author
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. Moved to the top in alphabetical order |
||
| }, | ||
| configs: { | ||
| base: require('./config/base'), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| 'use strict'; | ||
|
|
||
| // const ember = require('../utils/ember'); | ||
| const utils = require('../utils/utils'); | ||
|
|
||
| const ERROR_MESSAGE = 'All `addEventListener` calls should have a corresponding `removeEventListener` to avoid leaking callbacks.'; | ||
| //------------------------------------------------------------------------------ | ||
| // Ember object rule - Avoid callback memory leaks | ||
|
Member
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. Should this rule be limited to Ember objects only? If it should, it would need a If it's not necessary to limit it to Ember objects, and we can just run it on a per-file basis, then we would want to rename the rule to avoid mentioning
Contributor
Author
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 am afraid we can't directly use the And if we are not limiting this to Ember objects, is it OK to rename the rule to `avoid-leaking-callbacks'
Member
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. Is there a reason why this rule is specific to Ember? If this rule is not specific to Ember, then it could just be a generic JavaScript eslint rule, and likely would not belong in eslint-plugin-ember. (I found a similar rule here) If this rule is specific to Ember, then we need to update the code to actually limit it to something like Ember objects/components. In this case, you could write a helper function like
Member
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 tend to agree. I don't see a reason to scope this to just Ember objects.
Member
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. @rajasegar I wonder if you could try contributing this rule to eslint itself as a general
Contributor
Author
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. Sure @bmish I will try |
||
| // Callback leaks are memory leaks that occur due to state being caught | ||
| // in a callback function that is never released from memory. | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| module.exports = { | ||
| meta: { | ||
| docs: { | ||
| description: 'Avoids callback memory leaks', | ||
| category: 'Ember Object', | ||
| recommended: true, | ||
|
Member
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. Rules should never be added as recommended since that would be a breaking change. We can consider making it recommended in the future.
Contributor
Author
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. Removed the recommended flag |
||
| url: 'https://github.com/ember-best-practices/memory-leak-examples/blob/master/exercises/exercise-2.md' | ||
| }, | ||
| fixable: null, // or "code" or "whitespace" | ||
| ERROR_MESSAGE, | ||
| }, | ||
|
|
||
|
|
||
| create(context) { | ||
| const report = function (node) { | ||
| context.report(node, ERROR_MESSAGE); | ||
| }; | ||
|
|
||
| const addedListeners = []; | ||
| const removedListeners = []; | ||
| return { | ||
| 'Program:exit': function () { | ||
| addedListeners.forEach((a) => { | ||
| const idx = removedListeners.findIndex(r => r.el === a.el && r.event === a.event); | ||
|
|
||
| if (idx < 0) { | ||
| // No removeEventListener | ||
| report(a.node); | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
| CallExpression(node) { | ||
| if (utils.getPropertyValue(node, 'callee.property.name') === 'addEventListener') { | ||
| const listener = { | ||
| el: node.callee.object.name, | ||
| event: node.arguments[0].value, | ||
| node | ||
| }; | ||
|
|
||
| addedListeners.push(listener); | ||
| } | ||
|
|
||
| if (utils.getPropertyValue(node, 'callee.property.name') === 'removeEventListener') { | ||
| const listener = { | ||
| el: node.callee.object.name, | ||
| event: node.arguments[0].value | ||
| }; | ||
|
|
||
| removedListeners.push(listener); | ||
| } | ||
| } | ||
| }; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,5 +22,6 @@ Array [ | |
| "require-super-in-init", | ||
| "routes-segments-snake-case", | ||
| "use-brace-expansion", | ||
| "avoid-leaking-callbacks-in-ember-objects", | ||
|
Member
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. Remove this change.
Contributor
Author
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. done |
||
| ] | ||
| `; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // ------------------------------------------------------------------------------ | ||
| // Requirements | ||
| // ------------------------------------------------------------------------------ | ||
|
|
||
| const rule = require('../../../lib/rules/avoid-leaking-callbacks-in-ember-objects'); | ||
| const RuleTester = require('eslint').RuleTester; | ||
|
|
||
| const { ERROR_MESSAGE } = rule; | ||
| // ------------------------------------------------------------------------------ | ||
| // Tests | ||
| // ------------------------------------------------------------------------------ | ||
|
|
||
|
|
||
| const eslintTester = new RuleTester({ | ||
| parserOptions: { | ||
| ecmaVersion: 2018, | ||
| sourceType: 'module' | ||
| } | ||
| }); | ||
| eslintTester.run('avoid-leaking-callbacks-in-ember-objects', rule, { | ||
| valid: [ | ||
| { | ||
| code: ` | ||
| export default Ember.Component.extend({ | ||
|
Member
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 test case seems to duplicate the other valid test case. Let's keep the
Contributor
Author
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. Duplicate case removed. |
||
| didInsertElement() { | ||
| if (this.get('onScroll')) { | ||
| this._onScrollHandler = (...args) => this.get('onScroll')(...args); | ||
| window.addEventListener('scroll', this._onScrollHandler); | ||
| } | ||
| }, | ||
|
|
||
| willDestroyElement() { | ||
| window.removeEventListener("scroll", this._onScrollHandler); | ||
| this._super(...arguments); | ||
| } | ||
| });`, | ||
| }, | ||
| { | ||
| code: ` | ||
| import Component from '@ember/component'; | ||
|
|
||
| export default Component.extend({ | ||
| didInsertElement() { | ||
| if (this.get('onScroll')) { | ||
| this._onScrollHandler = (...args) => this.get('onScroll')(...args); | ||
| window.addEventListener('scroll', this._onScrollHandler); | ||
| } | ||
| }, | ||
|
|
||
| willDestroyElement() { | ||
| window.removeEventListener("scroll", this._onScrollHandler); | ||
| this._super(...arguments); | ||
| } | ||
| }); | ||
| `, | ||
| }, | ||
| { | ||
| code: ` | ||
| import Component from '@ember/component'; | ||
|
|
||
| export default Component.extend({});`, | ||
| }, | ||
| { | ||
| code: ` | ||
| export default Ember.Service.extend({ | ||
| init() { | ||
| if (this.get('onScroll')) { | ||
| this._onScrollHandler = (...args) => this.get('onScroll')(...args); | ||
| window.addEventListener('scroll', this._onScrollHandler); | ||
| } | ||
| }, | ||
|
|
||
| willDestroy() { | ||
| window.removeEventListener("scroll", this._onScrollHandler); | ||
| this._super(...arguments); | ||
| } | ||
| });`, | ||
| }, | ||
| ], | ||
| invalid: [ | ||
| { | ||
| code: ` | ||
| import Component from '@ember/component'; | ||
|
|
||
| export default Component.extend({ | ||
| didInsertElement() { | ||
| if (this.get('onScroll')) { | ||
| window.addEventListener('scroll', (...args) => this.get('onScroll')(...args)); | ||
| } | ||
| } | ||
| });`, | ||
| output: null, | ||
| errors: [{ | ||
| message: ERROR_MESSAGE, | ||
| }], | ||
| }, | ||
| { | ||
| code: ` | ||
| import Component from '@ember/component'; | ||
| export default Component.extend({ | ||
| init() { | ||
| if (this.get('onScroll')) { | ||
| window.addEventListener('scroll', (...args) => this.get('onScroll')(...args)); | ||
| } | ||
| }, | ||
|
|
||
| didInsertElement() { | ||
| if (this.get("onResize")) { | ||
| window.addEventListener("resize", (...args) => this.get("onResize")(...args)); | ||
| } | ||
| } | ||
|
|
||
| }); | ||
| `, | ||
| output: null, | ||
| errors: [{ | ||
| message: ERROR_MESSAGE, | ||
| }, | ||
| { | ||
| message: ERROR_MESSAGE, | ||
| }], | ||
| }, | ||
| { | ||
| code: ` | ||
| import Component from '@ember/component'; | ||
|
|
||
| export default Component.extend({ | ||
| init() { | ||
| if (this.get('onScroll')) { | ||
| this._onScrollHandler = (...args) => this.get('onScroll')(...args); | ||
| window.addEventListener('scroll', this._onScrollHandler); | ||
| } | ||
| }, | ||
|
|
||
|
|
||
| didInsertElement() { | ||
| if (this.get('onResize')) { | ||
| window.addEventListener('resize', (...args) => this.get('onResize')(...args)); | ||
| } | ||
| }, | ||
|
|
||
| willDestroyElement() { | ||
| window.removeEventListener("scroll", this._onScrollHandler); | ||
| this._super(...arguments); | ||
| } | ||
|
|
||
| }); | ||
| `, | ||
| output: null, | ||
| errors: [{ | ||
| message: ERROR_MESSAGE, | ||
| }], | ||
| }, | ||
|
|
||
|
|
||
| ], | ||
| }); | ||
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 don't think this section was supposed to move.
yarn updateshould generate this file for you.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 think after running yarn update only moved this block, i don't remember manually doing it. Anyway restored the position for now