Repository: cypress-io/eslint-plugin-cypress (js)

Issue:
error: no-assigning-return-values: Do not assign return value of Cy command
I just added the `eslint-plugin-cypress` to the Cypress documentation. So now our linter is running against our examples in our documentation. 

There are some examples we have in the docs that are erroring in the eslint-plugin, which I have to think should not be erroring since they are recommended uses of cypress. Can we make this rule exclude assignments of `cy.spy` or `cy.stub`?

1. `error  cypress/no-assigning-return-values: Do not assign the return value of a Cypress command` There are other examples in stub as well like `const stub = cy.stub()`
  https://docs.cypress.io/api/commands/spy.html#Aliases
  ```js
  const obj = {
    foo () {}
  }
  const spy = cy.spy(obj, 'foo').as('anyArgs')
  const withFoo = spy.withArgs('foo').as('withFoo')
  
  obj.foo()
  expect(spy).to.be.called
  expect(withFoo).to.be.called // purposefully failing assertion
  ```

Pull request description:
Whitelist returnable commands
Closes #12

Patch:
diff --git a/lib/config/recommended.js b/lib/config/recommended.js
index a5f8030..fce4052 100644
--- a/lib/config/recommended.js
+++ b/lib/config/recommended.js
@@ -1,9 +1,9 @@
-'use strict';
+'use strict'
 
 module.exports = {
   plugins: ['cypress'],
   rules: {
     'cypress/no-assigning-return-values': 'error',
-    'cypress/no-unnecessary-waiting': 'error'
-  }
-};
+    'cypress/no-unnecessary-waiting': 'error',
+  },
+}
diff --git a/lib/rules/no-assigning-return-values.js b/lib/rules/no-assigning-return-values.js
index e2119b4..5c6b90f 100644
--- a/lib/rules/no-assigning-return-values.js
+++ b/lib/rules/no-assigning-return-values.js
@@ -3,7 +3,18 @@
  * @author Elad Shahar
  */
 
-'use strict';
+'use strict'
+
+// safely get nested object property
+function get (obj, propertyString = '') {
+  const properties = propertyString.split('.')
+  for (let i = 0; i < properties.length; i++) {
+    const value = (obj || {})[properties[i]]
+    if (value == null) return value
+    obj = value
+  }
+  return obj
+}
 
 module.exports = {
   meta: {
@@ -11,39 +22,45 @@ module.exports = {
       description: 'Prevent assigning return values of cy calls',
       category: 'Possible Errors',
       recommended: true,
-      url: 'https://docs.cypress.io/guides/references/best-practices.html#Assigning-Return-Values'
+      url: 'https://docs.cypress.io/guides/references/best-practices.html#Assigning-Return-Values',
     },
     schema: [],
     messages: {
-      unexpected: 'Do not assign the return value of a Cypress command'
-    }
+      unexpected: 'Do not assign the return value of a Cypress command',
+    },
   },
-  create(context) {
+  create (context) {
     return {
-      VariableDeclaration(node) {
+      VariableDeclaration (node) {
         if (node.declarations.some(isCypressCommandDeclaration)) {
-          context.report({ node, messageId: 'unexpected' });
+          context.report({ node, messageId: 'unexpected' })
         }
-      }
-    };
-  }
-};
+      },
+    }
+  },
+}
 
-function isCypressCommandDeclaration(declarator) {
-  if (!declarator)             { return; }
-  if (!declarator.init)        { return; }
-  if (!declarator.init.callee) { return; }
+const whitelistedCommands = {
+  now: true,
+  spy: true,
+  state: true,
+  stub: true,
+}
 
-  let object = declarator.init.callee.object;
+function isCypressCommandDeclaration (declarator) {
+  let object = get(declarator, 'init.callee.object')
 
-  if (!object)                 { return; }
+  if (!object) return
 
   while (object.callee) {
-    object = object.callee.object;
-    if (!object) {
-      return;
-    }
+    object = object.callee.object
+
+    if (!object) return
   }
 
-  return object.name === 'cy';
+  const commandName = get(object, 'parent.property.name')
+
+  if (commandName && whitelistedCommands[commandName]) return
+
+  return object.name === 'cy'
 }
diff --git a/lib/rules/no-unnecessary-waiting.js b/lib/rules/no-unnecessary-waiting.js
index 45b8e7e..1204065 100644
--- a/lib/rules/no-unnecessary-waiting.js
+++ b/lib/rules/no-unnecessary-waiting.js
@@ -3,7 +3,7 @@
  * @author Elad Shahar
  */
 
-'use strict';
+'use strict'
 
 module.exports = {
   meta: {
@@ -11,34 +11,34 @@ module.exports = {
       description: 'Prevent waiting for arbitrary time periods',
       category: 'Possible Errors',
       recommended: true,
-      url: 'https://docs.cypress.io/guides/references/best-practices.html#Unnecessary-Waiting'
+      url: 'https://docs.cypress.io/guides/references/best-practices.html#Unnecessary-Waiting',
     },
     schema: [],
     messages: {
-      unexpected: 'Do not wait for arbitrary time periods'
-    }
+      unexpected: 'Do not wait for arbitrary time periods',
+    },
   },
-  create(context) {
+  create (context) {
     return {
-      CallExpression(node) {
+      CallExpression (node) {
         if (isCallingCyWait(node) && isNumberArgument(node)) {
-          context.report({ node, messageId: 'unexpected' });
+          context.report({ node, messageId: 'unexpected' })
         }
-      }
-    };
-  }
-};
+      },
+    }
+  },
+}
 
-function isCallingCyWait(node) {
+function isCallingCyWait (node) {
   return node.callee.type === 'MemberExpression' &&
          node.callee.object.type === 'Identifier' &&
          node.callee.object.name === 'cy' &&
          node.callee.property.type === 'Identifier' &&
-         node.callee.property.name === 'wait';
+         node.callee.property.name === 'wait'
 }
 
-function isNumberArgument(node) {
+function isNumberArgument (node) {
   return node.arguments.length > 0 &&
          node.arguments[0].type === 'Literal' &&
-         typeof(node.arguments[0].value) === 'number';
+         typeof (node.arguments[0].value) === 'number'
 }


Test patch:
diff --git a/tests/lib/rules/no-assigning-return-values.js b/tests/lib/rules/no-assigning-return-values.js
index d32ee55..8218d12 100644
--- a/tests/lib/rules/no-assigning-return-values.js
+++ b/tests/lib/rules/no-assigning-return-values.js
@@ -1,12 +1,12 @@
-"use strict";
+'use strict'
 
-const rule = require('../../../lib/rules/no-assigning-return-values');
-const RuleTester = require('eslint').RuleTester;
+const rule = require('../../../lib/rules/no-assigning-return-values')
+const RuleTester = require('eslint').RuleTester
 
-const ruleTester = new RuleTester();
+const ruleTester = new RuleTester()
 
-const errors = [{ messageId: 'unexpected' }];
-const parserOptions = { ecmaVersion: 6 };
+const errors = [{ messageId: 'unexpected' }]
+const parserOptions = { ecmaVersion: 6 }
 
 ruleTester.run('no-assigning-return-values', rule, {
   valid: [
@@ -15,8 +15,12 @@ ruleTester.run('no-assigning-return-values', rule, {
     { code: 'const foo = true;', parserOptions },
     { code: 'const foo = bar();', parserOptions },
     { code: 'const foo = bar().baz();', parserOptions },
+    { code: 'const spy = cy.spy();', parserOptions },
+    { code: 'const stub = cy.stub();', parserOptions },
+    { code: 'const result = cy.now();', parserOptions },
+    { code: 'const state = cy.state();', parserOptions },
     { code: 'cy.get("foo");', parserOptions },
-    { code: 'cy.contains("foo").click();', parserOptions }
+    { code: 'cy.contains("foo").click();', parserOptions },
   ],
 
   invalid: [
@@ -28,6 +32,6 @@ ruleTester.run('no-assigning-return-values', rule, {
     { code: 'let a = cy.window()', parserOptions, errors },
     { code: 'let a = cy.wait("@something")', parserOptions, errors },
 
-    { code: 'let a = cy.contains("foo").click()', parserOptions, errors }
-  ]
-});
+    { code: 'let a = cy.contains("foo").click()', parserOptions, errors },
+  ],
+})
diff --git a/tests/lib/rules/no-unnecessary-waiting.js b/tests/lib/rules/no-unnecessary-waiting.js
index a7ad8bf..5532f71 100644
--- a/tests/lib/rules/no-unnecessary-waiting.js
+++ b/tests/lib/rules/no-unnecessary-waiting.js
@@ -1,12 +1,12 @@
-"use strict";
+'use strict'
 
-const rule = require('../../../lib/rules/no-unnecessary-waiting');
-const RuleTester = require('eslint').RuleTester;
+const rule = require('../../../lib/rules/no-unnecessary-waiting')
+const RuleTester = require('eslint').RuleTester
 
-const ruleTester = new RuleTester();
+const ruleTester = new RuleTester()
 
-const errors = [{ messageId: 'unexpected' }];
-const parserOptions = { ecmaVersion: 6 };
+const errors = [{ messageId: 'unexpected' }]
+const parserOptions = { ecmaVersion: 6 }
 
 ruleTester.run('no-unnecessary-waiting', rule, {
   valid: [
@@ -15,14 +15,14 @@ ruleTester.run('no-unnecessary-waiting', rule, {
     { code: 'cy.wait("@someRequest").then((xhr) => xhr)', parserOptions },
     { code: 'cy.wait(["@someRequest", "@anotherRequest"])', parserOptions },
 
-    { code: 'cy.clock(5000)', parserOptions},
+    { code: 'cy.clock(5000)', parserOptions },
     { code: 'cy.scrollTo(0, 10)', parserOptions },
-    { code: 'cy.tick(500)', parserOptions }
+    { code: 'cy.tick(500)', parserOptions },
   ],
 
   invalid: [
     { code: 'cy.wait(0)', parserOptions, errors },
     { code: 'cy.wait(100)', parserOptions, errors },
-    { code: 'cy.wait(5000)', parserOptions, errors }
-  ]
-});
+    { code: 'cy.wait(5000)', parserOptions, errors },
+  ],
+})