Repository: ember-cli/eslint-plugin-ember (js)

Issue:
New Rule: `no-incorrect-computed-macros`
Looks for incorrect usages of [computed property macros](https://api.emberjs.com/ember/3.16/classes/@ember%2Fobject%2Fcomputed). Disallows calling computed property macros with the wrong number of parameters or wrong type of parameters.

Example: calling `and` or `or` with < 2 parameters. If these have only one parameter, they should use `readOnly` instead.

Pull request description:
Add new rule `no-incorrect-computed-macros`
This rule attempts to find incorrect usages of computed property macros, such as calling them with the incorrect number of arguments.

It currently only catches using the [and](https://api.emberjs.com/ember/release/functions/@ember%2Fobject%2Fcomputed/and) and [or](https://api.emberjs.com/ember/release/functions/@ember%2Fobject%2Fcomputed/or) macros with the wrong number of arguments, but can be expanded later.

Fixes #685.

Patch:
diff --git a/README.md b/README.md
index de1e4613..e159f2d5 100644
--- a/README.md
+++ b/README.md
@@ -113,6 +113,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 | :white_check_mark::wrench: | [no-ember-super-in-es-classes](./docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods |
 | :white_check_mark: | [no-ember-testing-in-module-scope](./docs/rules/no-ember-testing-in-module-scope.md) | disallow use of `Ember.testing` in module scope |
 | :white_check_mark: | [no-incorrect-calls-with-inline-anonymous-functions](./docs/rules/no-incorrect-calls-with-inline-anonymous-functions.md) | disallow inline anonymous functions as arguments to `debounce`, `once`, and `scheduleOnce` |
+| :wrench: | [no-incorrect-computed-macros](./docs/rules/no-incorrect-computed-macros.md) | disallow incorrect usage of computed property macros |
 | :white_check_mark: | [no-invalid-debug-function-arguments](./docs/rules/no-invalid-debug-function-arguments.md) | disallow usages of Ember's `assert()` / `warn()` / `deprecate()` functions that have the arguments passed in the wrong order. |
 | :white_check_mark: | [no-side-effects](./docs/rules/no-side-effects.md) | disallow unexpected side effects in computed properties |
 | :wrench: | [require-computed-property-dependencies](./docs/rules/require-computed-property-dependencies.md) | require dependencies to be declared statically in computed properties |
diff --git a/docs/rules/no-incorrect-computed-macros.md b/docs/rules/no-incorrect-computed-macros.md
new file mode 100644
index 00000000..7476a1f3
--- /dev/null
+++ b/docs/rules/no-incorrect-computed-macros.md
@@ -0,0 +1,44 @@
+# no-incorrect-computed-macros
+
+:wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+This rule attempts to find incorrect usages of computed property macros, such as calling them with the incorrect number of arguments.
+
+It currently only catches using the [and](https://api.emberjs.com/ember/release/functions/@ember%2Fobject%2Fcomputed/and) and [or](https://api.emberjs.com/ember/release/functions/@ember%2Fobject%2Fcomputed/or) macros with the wrong number of arguments, but may be expanded later.
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```js
+import { and, or } from '@ember/object/computed';
+
+export default Component.extend({
+  macroPropertyAnd: and('someProperty'), // Not enough arguments.
+
+  macroPropertyOr: or('someProperty') // Not enough arguments.
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import { and, or, readOnly } from '@ember/object/computed';
+
+export default Component.extend({
+  macroPropertyReadOnly: readOnly('someProperty'),
+
+  macroPropertyAnd: and('someProperty1', 'someProperty2'),
+
+  macroPropertyOr: or('someProperty1', 'someProperty2')
+});
+```
+
+## Related Rules
+
+* [require-computed-macros](require-computed-macros.md)
+
+## References
+
+* [Guide](https://guides.emberjs.com/release/object-model/computed-properties/) for computed properties
+* [Spec](http://api.emberjs.com/ember/release/modules/@ember%2Fobject#functions-computed) for computed property macros
diff --git a/docs/rules/require-computed-macros.md b/docs/rules/require-computed-macros.md
index 7bb76d71..6b820d75 100644
--- a/docs/rules/require-computed-macros.md
+++ b/docs/rules/require-computed-macros.md
@@ -92,6 +92,10 @@ export default Component.extend({
 * [Guide](https://guides.emberjs.com/release/object-model/computed-properties/) for computed properties
 * [Spec](http://api.emberjs.com/ember/release/modules/@ember%2Fobject#functions-computed) for computed property macros
 
+## Related Rules
+
+* [no-incorrect-computed-macros](no-incorrect-computed-macros.md)
+
 ## Help Wanted
 
 | Issue | Link |
diff --git a/lib/index.js b/lib/index.js
index 65c054c2..faf4cf79 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -32,6 +32,7 @@ module.exports = {
     'no-get': require('./rules/no-get'),
     'no-global-jquery': require('./rules/no-global-jquery'),
     'no-incorrect-calls-with-inline-anonymous-functions': require('./rules/no-incorrect-calls-with-inline-anonymous-functions'),
+    'no-incorrect-computed-macros': require('./rules/no-incorrect-computed-macros'),
     'no-invalid-debug-function-arguments': require('./rules/no-invalid-debug-function-arguments'),
     'no-jquery': require('./rules/no-jquery'),
     'no-legacy-test-waiters': require('./rules/no-legacy-test-waiters'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index c90060e2..77796f5b 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -35,6 +35,7 @@ module.exports = {
   "ember/no-get": "off",
   "ember/no-global-jquery": "error",
   "ember/no-incorrect-calls-with-inline-anonymous-functions": "error",
+  "ember/no-incorrect-computed-macros": "off",
   "ember/no-invalid-debug-function-arguments": "error",
   "ember/no-jquery": "off",
   "ember/no-legacy-test-waiters": "off",
diff --git a/lib/rules/no-incorrect-computed-macros.js b/lib/rules/no-incorrect-computed-macros.js
new file mode 100644
index 00000000..194f0f10
--- /dev/null
+++ b/lib/rules/no-incorrect-computed-macros.js
@@ -0,0 +1,58 @@
+'use strict';
+
+const { getImportIdentifier } = require('../utils/import');
+const types = require('../utils/types');
+
+const ERROR_MESSAGE_AND_OR = 'Computed property macro should be used with 2+ arguments';
+
+const COMPUTED_MACROS_AND_OR = ['and', 'or'];
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow incorrect usage of computed property macros',
+      category: 'Possible Errors',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-incorrect-computed-macros.md',
+    },
+    fixable: 'code',
+    schema: [],
+  },
+
+  ERROR_MESSAGE_AND_OR,
+
+  create(context) {
+    let macroIdentifiersAndOr = [];
+
+    return {
+      ImportDeclaration(node) {
+        if (node.source.value !== '@ember/object/computed') {
+          return;
+        }
+
+        // Gather the identifiers that these macros are imported under.
+        macroIdentifiersAndOr = COMPUTED_MACROS_AND_OR.map(fn =>
+          getImportIdentifier(node, '@ember/object/computed', fn)
+        );
+      },
+
+      CallExpression(node) {
+        if (types.isIdentifier(node.callee) && macroIdentifiersAndOr.includes(node.callee.name)) {
+          if (node.arguments.length === 1 && !types.isSpreadElement(node.arguments[0])) {
+            context.report({
+              node: node.callee,
+              message: ERROR_MESSAGE_AND_OR,
+              fix(fixer) {
+                return fixer.replaceText(node.callee, 'readOnly');
+              },
+            });
+          } else if (node.arguments.length === 0) {
+            context.report(node, ERROR_MESSAGE_AND_OR);
+          }
+        }
+      },
+    };
+  },
+};
diff --git a/lib/utils/types.js b/lib/utils/types.js
index 07c31e14..d9797454 100644
--- a/lib/utils/types.js
+++ b/lib/utils/types.js
@@ -28,6 +28,7 @@ module.exports = {
   isObjectPattern,
   isProperty,
   isReturnStatement,
+  isSpreadElement,
   isString,
   isStringLiteral,
   isTaggedTemplateExpression,
@@ -326,6 +327,16 @@ function isReturnStatement(node) {
   return node !== undefined && node.type && node.type === 'ReturnStatement';
 }
 
+/**
+ * Check whether or not a node is a SpreadElement
+ *
+ * @param {Object} node The node to check.
+ * @return {Boolean} Whether or not the node is a SpreadElement.
+ */
+function isSpreadElement(node) {
+  return node !== undefined && node.type === 'SpreadElement';
+}
+
 function isString(node) {
   return isTemplateLiteral(node) || (isLiteral(node) && typeof node.value === 'string');
 }


Test patch:
diff --git a/tests/lib/rules/no-incorrect-computed-macros.js b/tests/lib/rules/no-incorrect-computed-macros.js
new file mode 100644
index 00000000..50682c5c
--- /dev/null
+++ b/tests/lib/rules/no-incorrect-computed-macros.js
@@ -0,0 +1,126 @@
+'use strict';
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-incorrect-computed-macros');
+const RuleTester = require('eslint').RuleTester;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const { ERROR_MESSAGE_AND_OR } = rule;
+const ruleTester = new RuleTester({
+  parser: require.resolve('babel-eslint'),
+  parserOptions: {
+    ecmaVersion: 6,
+    sourceType: 'module',
+    ecmaFeatures: { legacyDecorators: true },
+  },
+});
+
+ruleTester.run('no-incorrect-computed-macros', rule, {
+  valid: [
+    // Correct usage:
+    `import { and } from '@ember/object/computed';
+     and('someProperty1', 'someProperty2')`,
+    `import { or } from '@ember/object/computed';
+     or('someProperty1', 'someProperty2')`,
+
+    // Spread element:
+    `import { and } from '@ember/object/computed';
+     and(...deps)`,
+
+    // Wrong function:
+    `import { and } from '@ember/object/computed';
+     and.random()`,
+    `import { or } from '@ember/object/computed';
+     or.random()`,
+
+    // Wrong function:
+    `import { and } from '@ember/object/computed';
+     random.and()`,
+    `import { and } from '@ember/object/computed';
+    random.or()`,
+
+    // Not from the right import.
+    'and()',
+    'or()',
+
+    // One test case with decorators:
+    `import { and } from '@ember/object/computed';
+     class Test { @and('someProperty1', 'someProperty2') prop }`,
+  ],
+
+  invalid: [
+    {
+      code: `
+      import { and } from '@ember/object/computed';
+      and()`,
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE_AND_OR,
+          type: 'CallExpression',
+        },
+      ],
+    },
+    {
+      code: `
+      import { or } from '@ember/object/computed';
+      or()`,
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE_AND_OR,
+          type: 'CallExpression',
+        },
+      ],
+    },
+    {
+      code: `
+      import { and } from '@ember/object/computed';
+      and('someProperty')`,
+      output: `
+      import { and } from '@ember/object/computed';
+      readOnly('someProperty')`,
+      errors: [
+        {
+          message: ERROR_MESSAGE_AND_OR,
+          type: 'Identifier',
+        },
+      ],
+    },
+    {
+      code: `
+      import { or } from '@ember/object/computed';
+      or('someProperty')`,
+      output: `
+      import { or } from '@ember/object/computed';
+      readOnly('someProperty')`,
+      errors: [
+        {
+          message: ERROR_MESSAGE_AND_OR,
+          type: 'Identifier',
+        },
+      ],
+    },
+    {
+      // One test case with decorators:
+      code: `
+      import { and } from '@ember/object/computed';
+      class Test { @and('someProperty') prop }`,
+      output: `
+      import { and } from '@ember/object/computed';
+      class Test { @readOnly('someProperty') prop }`,
+      errors: [
+        {
+          message: ERROR_MESSAGE_AND_OR,
+          type: 'Identifier',
+        },
+      ],
+    },
+  ],
+});