Repository: vuejs/eslint-plugin-vue
Pull request #72 (2017-07-08 13:58:40)

Add `no-async-in-computed-properties` rule
This PR implements rules proposed in #53 

**DONE:**
* [x] Create documtation
* [x] Add tests
* [x] `async` `await`
* [x] `new Promise`
* [x] `.then` `.catch` `.finally`
* [x] `Promise.all()` `Promise.race()` `Promise.reject()` `Promise.resolve()`
* [x] `setTimeout()` `setInterval()` `setImmediate()` `requestAnimationFrame()`


armano2: @michalsnik There is one commit there with you as author (i took some of your changes) to align with it 🗡 

I cleaned up commits from merges and most of rules are working now 🔢 
michalsnik: Great @armano2, thank you so much for all the hard work! I'll review it in details later today, but so far it looks really good! :) I'm glad you figured out more cases, these all make perfect sense to me :)
armano2: Thank you for code review i adapted all requests into code except chained promises
michalsnik: I put `do not merge` label on all of your rules. And I'll start merging/finishing reviewing them after #69 is merged, to have a better picture of your changes.
armano2: ok 
michalsnik: Alright, you can fix conflicts now :)
armano2: @michalsnik cleaned up and merged
mysticatea: I'm not sure this rule should disallow `yield` expressions.
This is not related to async. But it might be dangerous since the value is cached...

```js
export default {
    computed: {
        *numbers() {
            yield 1
            yield 2
            yield 3
        }
    }
}
```
armano2: > await nodes are YieldExpression's with babel-eslint < 7.0.0

i don't like this to but `babel-eslint < 7.0.0` 👎 
mysticatea: @armano2 I see. Hmm, how about disallowing `async` functions instead of `await` expressions? `babel-eslint@6` looks to have `node.async === true` for `async` functions correctly. The `await` expressions on outside of `async` functions are syntax error.
armano2: @mysticatea yes its true and we `node.async === true` is already supported

Doc: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield

> The yield keyword causes generator function execution to **pause** and the value of the expression following the yield keyword is returned to the generator's caller. It can be thought of as a generator-based version of the return keyword.

For me yield can be called async-like.
> A yield, which causes the generator to once again pause and return the generator's new `value`. The next time `next()` is called, **execution resumes with the statement** immediately after the `yield`.

but if you want i can remote it from support.

michalsnik: Hmm, I'm ok with both versions but to be honest, who would ever want to set generator as computed property? It doesn't make sense at all. I think we can even remote this check and maybe add another rule for that but personally I don't think it's even necessary.. What do you think guys?
armano2: @michalsnik yield/generator removed 🍡 
michalsnik: Okay, one last thing @armano2 let's change the `await` check to `async` function detection and it's good to go.
armano2: @michalsnik but we are checking `async` function declaration already here: https://github.com/vuejs/eslint-plugin-vue/pull/72/files#diff-ceb2d093cf4ac93cc581e9af530bfebaR70

Patch:
diff --git a/docs/rules/no-async-in-computed-properties.md b/docs/rules/no-async-in-computed-properties.md
new file mode 100644
index 00000000..83c48b59
--- /dev/null
+++ b/docs/rules/no-async-in-computed-properties.md
@@ -0,0 +1,60 @@
+# Check if there are no asynchronous actions inside computed properties (no-async-in-computed-properties)
+
+Computed properties should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them.
+If you need async computed properties you might want to consider using additional plugin [vue-async-computed]
+
+## :book: Rule Details
+
+This rule is aimed at preventing asynchronous methods from being called in computed properties.
+
+:-1: Examples of **incorrect** code for this rule:
+
+```js
+export default {
+  computed: {
+    pro () {
+      return Promise.all([new Promise((resolve, reject) => {})])
+    },
+    foo: async function () {
+      return await someFunc()
+    },
+    bar () {
+      return fetch(url).then(response => {})
+    },
+    tim () {
+      setTimeout(() => { }, 0)
+    },
+    inter () {
+      setInterval(() => { }, 0)
+    },
+    anim () {
+      requestAnimationFrame(() => {})
+    }
+  }
+}
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```js
+export default {
+  computed: {
+    foo () {
+      var bar = 0
+      try {
+        bar = bar / this.a
+      } catch (e) {
+        return 0
+      } finally {
+        return bar
+      }
+    }
+  }
+}
+```
+
+## :wrench: Options
+
+Nothing.
+
+[vue-async-computed]: https://github.com/foxbenjaminfox/vue-async-computed
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
new file mode 100644
index 00000000..46e5e88e
--- /dev/null
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -0,0 +1,157 @@
+/**
+ * @fileoverview Check if there are no asynchronous actions inside computed properties.
+ * @author Armano
+ */
+'use strict'
+
+const utils = require('../utils')
+
+const PROMISE_FUNCTIONS = [
+  'then',
+  'catch',
+  'finally'
+]
+
+const PROMISE_METHODS = [
+  'all',
+  'race',
+  'reject',
+  'resolve'
+]
+
+const TIMED_FUNCTIONS = [
+  'setTimeout',
+  'setInterval',
+  'setImmediate',
+  'requestAnimationFrame'
+]
+
+function isTimedFunction (node) {
+  return (
+    node.type === 'CallExpression' &&
+    node.callee.type === 'Identifier' &&
+    TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1
+  ) || (
+    node.type === 'CallExpression' &&
+    node.callee.type === 'MemberExpression' &&
+    node.callee.object.type === 'Identifier' &&
+    node.callee.object.name === 'window' && (
+      TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1
+    )
+  ) && node.arguments.length
+}
+
+function isPromise (node) {
+  if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
+    return ( // hello.PROMISE_FUNCTION()
+      node.callee.property.type === 'Identifier' &&
+      PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1
+    ) || ( // Promise.PROMISE_METHOD()
+      node.callee.object.type === 'Identifier' &&
+      node.callee.object.name === 'Promise' &&
+      PROMISE_METHODS.indexOf(node.callee.property.name) !== -1
+    )
+  }
+  return false
+}
+
+function create (context) {
+  const forbiddenNodes = []
+
+  const expressionTypes = {
+    promise: 'asynchronous action',
+    await: 'await operator',
+    async: 'async function declaration',
+    new: 'Promise object',
+    timed: 'timed function'
+  }
+
+  function onFunctionEnter (node) {
+    if (node.async) {
+      forbiddenNodes.push({
+        node: node,
+        type: 'async'
+      })
+    }
+  }
+
+  return Object.assign({},
+    {
+      FunctionDeclaration: onFunctionEnter,
+
+      FunctionExpression: onFunctionEnter,
+
+      ArrowFunctionExpression: onFunctionEnter,
+
+      NewExpression (node) {
+        if (node.callee.name === 'Promise') {
+          forbiddenNodes.push({
+            node: node,
+            type: 'new'
+          })
+        }
+      },
+
+      CallExpression (node) {
+        if (isPromise(node)) {
+          forbiddenNodes.push({
+            node: node,
+            type: 'promise'
+          })
+        }
+        if (isTimedFunction(node)) {
+          forbiddenNodes.push({
+            node: node,
+            type: 'timed'
+          })
+        }
+      },
+
+      AwaitExpression (node) {
+        forbiddenNodes.push({
+          node: node,
+          type: 'await'
+        })
+      }
+    },
+    utils.executeOnVueComponent(context, (obj) => {
+      const computedProperties = utils.getComputedProperties(obj)
+
+      computedProperties.forEach(cp => {
+        forbiddenNodes.forEach(el => {
+          if (
+            cp.value &&
+            el.node.loc.start.line >= cp.value.loc.start.line &&
+            el.node.loc.end.line <= cp.value.loc.end.line
+          ) {
+            context.report({
+              node: el.node,
+              message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
+              data: {
+                expressionName: expressionTypes[el.type],
+                propertyName: cp.key
+              }
+            })
+          }
+        })
+      })
+    })
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  create,
+  meta: {
+    docs: {
+      description: 'Check if there are no asynchronous actions inside computed properties.',
+      category: 'Best Practices',
+      recommended: false
+    },
+    fixable: null,
+    schema: []
+  }
+}
diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js
index 968f7a81..beb928ad 100644
--- a/lib/rules/no-side-effects-in-computed-properties.js
+++ b/lib/rules/no-side-effects-in-computed-properties.js
@@ -39,6 +39,7 @@ function create (context) {
       computedProperties.forEach(cp => {
         forbiddenNodes.forEach(node => {
           if (
+            cp.value &&
             node.loc.start.line >= cp.value.loc.start.line &&
             node.loc.end.line <= cp.value.loc.end.line
           ) {


Test patch:
diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js
new file mode 100644
index 00000000..34c57e75
--- /dev/null
+++ b/tests/lib/rules/no-async-in-computed-properties.js
@@ -0,0 +1,345 @@
+/**
+ * @fileoverview Check if there are no side effects inside computed properties
+ * @author 2017 Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-async-in-computed-properties')
+const RuleTester = require('eslint').RuleTester
+
+const parserOptions = {
+  ecmaVersion: 6,
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
+}
+
+const parserOptions8 = {
+  ecmaVersion: 8,
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+
+ruleTester.run('no-async-in-computed-properties', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            ...mapGetters({
+              test: 'getTest'
+            }),
+            foo: function () {
+              var bar = 0
+              try {
+                bar = bar / 0
+              } catch (e) {
+                return e
+              } finally {
+                return bar
+              }
+            },
+            foo2: {
+              set () {
+                new Promise((resolve, reject) => {})
+              }
+            }
+          }
+        }
+      `,
+      parserOptions
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: async function () {
+              return await someFunc()
+            }
+          }
+        }
+      `,
+      parserOptions: parserOptions8,
+      errors: [{
+        message: 'Unexpected async function declaration in "foo" computed property.',
+        line: 4
+      }, {
+        message: 'Unexpected await operator in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: async function () {
+              return new Promise((resolve, reject) => {})
+            }
+          }
+        }
+      `,
+      parserOptions: parserOptions8,
+      errors: [{
+        message: 'Unexpected async function declaration in "foo" computed property.',
+        line: 4
+      }, {
+        message: 'Unexpected Promise object in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              return bar.then(response => {})
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              return bar.catch(e => {})
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              return Promise.all([])
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              return bar.finally(res => {})
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: function () {
+            return Promise.race([])
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: function () {
+            return Promise.reject([])
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: function () {
+            return Promise.resolve([])
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo () {
+            return Promise.resolve([])
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 5
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: {
+            get () {
+              return Promise.resolve([])
+            }
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 6
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      new Vue({
+        computed: {
+          foo: {
+            get () {
+              return Promise.resolve([])
+            }
+          }
+        }
+      })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 6
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      new Vue({
+        computed: {
+          foo: {
+            get () {
+              return test.blabla.then([])
+            }
+          }
+        }
+      })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Unexpected asynchronous action in "foo" computed property.',
+        line: 6
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: function () {
+            setTimeout(() => { }, 0)
+            window.setTimeout(() => { }, 0)
+            setInterval(() => { }, 0)
+            window.setInterval(() => { }, 0)
+            setImmediate(() => { })
+            window.setImmediate(() => { })
+            requestAnimationFrame(() => {})
+            window.requestAnimationFrame(() => {})
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 5
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 6
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 7
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 8
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 9
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 10
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 11
+      }, {
+        message: 'Unexpected timed function in "foo" computed property.',
+        line: 12
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js
index 5a58de08..c6f95f15 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -27,6 +27,7 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
     {
       code: `Vue.component('test', {
         computed: {
+          ...test0({}),
           test1() {
             return this.firstName + ' ' + this.lastName
           },