diff --git a/README.md b/README.md index a9d86efa64..9de5fe9a93 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ The `--fix` option on the command line automatically fixes problems reported by | | Rule ID | Description | |:---|:--------|:------------| | :white_check_mark: | [jquery-ember-run](./docs/rules/jquery-ember-run.md) | Prevents usage of jQuery without Ember Run Loop | +| | [no-async-actions](./docs/rules/no-async-actions.md) | Disallow usage of async actions in components | | :white_check_mark: | [no-attrs-in-components](./docs/rules/no-attrs-in-components.md) | Disallow usage of this.attrs in components | | :white_check_mark: | [no-attrs-snapshot](./docs/rules/no-attrs-snapshot.md) | Disallow use of attrs snapshot in `didReceiveAttrs` and `didUpdateAttrs` | | :white_check_mark: | [no-capital-letters-in-routes](./docs/rules/no-capital-letters-in-routes.md) | Raise an error when there is a route with uppercased letters in router.js | diff --git a/docs/rules/no-async-actions.md b/docs/rules/no-async-actions.md new file mode 100644 index 0000000000..db676b21c9 --- /dev/null +++ b/docs/rules/no-async-actions.md @@ -0,0 +1,53 @@ +# Do not use async actions +## Rule `no-async-actions` + +Using async actions can lead to memory leaks and application errors if you +don't check for `isDestroying` and `isDestroyed` after each async step + + +Examples of **incorrect** code for this rule: +```js +actions: { + async handleClick() { + // ... + } +} +``` + +```js +actions: { + handleClick() { + return something.then(() => { /* ... */ }); + } +} +``` + +```js +@action +async handleClick() { + // ... +} +``` + +```js +@action +handleClick() { + return something.then(() => { /* ... */ }); +} +``` + + +Examples of **correct** code for this rule: +```js +actions: { + handleClick() { + return nothingOrSomethingThatIsNotAPromise; + } +} +``` + + +## Further Reading + +- Ember Concurrency http://ember-concurrency.com/docs/tutorial (scroll down to Version 4) + diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js index ba0fff71c3..85d7e270a3 100644 --- a/lib/recommended-rules.js +++ b/lib/recommended-rules.js @@ -15,6 +15,7 @@ module.exports = { "ember/local-modules": "off", "ember/named-functions-in-promises": "off", "ember/new-module-imports": "error", + "ember/no-async-actions": "off", "ember/no-attrs-in-components": "error", "ember/no-attrs-snapshot": "error", "ember/no-capital-letters-in-routes": "error", @@ -52,4 +53,4 @@ module.exports = { "ember/routes-segments-snake-case": "error", "ember/use-brace-expansion": "error", "ember/use-ember-get-and-set": "off" -} +} \ No newline at end of file diff --git a/lib/rules/no-async-actions.js b/lib/rules/no-async-actions.js new file mode 100644 index 0000000000..57f770de10 --- /dev/null +++ b/lib/rules/no-async-actions.js @@ -0,0 +1,61 @@ +'use strict'; + +const utils = require('../utils/utils'); + + +//------------------------------------------------------------------------------ +// General rule - Don't use async actions +//------------------------------------------------------------------------------ + + +const ERROR_MESSAGE = 'Do not use async actions.'; + +module.exports = { + meta: { + docs: { + description: 'Disallow usage of async actions in components', + category: 'Possible Errors', + url: 'http://ember-concurrency.com/docs/tutorial' + }, + fixable: null, + ERROR_MESSAGE, + }, + + create(context) { + return { + Property(node) { + if (node.key.name === 'actions') { + const props = node.value.properties; + + props.forEach((p) => { + const body = p.value.body.body; + if (p.value.async) { + context.report({ + node: p, + message: ERROR_MESSAGE, + }); + } else if (body.length === 1 && utils.isReturnStatement(body[0])) { + const retSt = body[0]; + if (retSt.argument.type === 'CallExpression' && + retSt.argument.callee.property.name === 'then') { + context.report({ + node: retSt, + message: ERROR_MESSAGE, + }); + } + } + }); + } else if (node.decorators) { + if (node.decorators.find(d => d.expression.name === 'action')) { + if (node.value.async) { + context.report({ + node, + message: ERROR_MESSAGE + }); + } + } + } + } + }; + } +}; diff --git a/tests/lib/rules/no-async-actions.js b/tests/lib/rules/no-async-actions.js new file mode 100644 index 0000000000..8d5372e99d --- /dev/null +++ b/tests/lib/rules/no-async-actions.js @@ -0,0 +1,92 @@ +'use strict'; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-async-actions.js'); +const RuleTester = require('eslint').RuleTester; + +const { ERROR_MESSAGE } = rule; +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + + +const ruleTester = new RuleTester({}); + +ruleTester.run('no-async-actions', rule, { + valid: [ + { + code: ` + Component.extend({ + actions: { + handleClick() { + // ... + } + } + });`, + } + + ], + + invalid: [ + { + code: `Component.extend({ + actions: { + async handleClick() { + // ... + } + } + });`, + output: null, + errors: [{ + message: ERROR_MESSAGE, + }] + }, + { + code: `Component.extend({ + actions: { + handleClick() { + return something.then(() => { + let hello = "world"; + }); + } + } + });`, + output: null, + errors: [{ + message: ERROR_MESSAGE, + }] + }, + { + code: `Component.extend({ + @action + async handleClick() { + // ... + } + });`, + parser: 'babel-eslint', + output: null, + errors: [{ + message: ERROR_MESSAGE, + }] + }, + { + code: `Component.extend({ + @action + handleClick() { + return something.then(() => { + let hello = "world"; + }); + } + });`, + parser: 'babel-eslint', + output: null, + errors: [{ + message: ERROR_MESSAGE, + }] + }, + + ] +});