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

Issue:
New rule: Lint against `tryInvoke`
There is an open RFC to deprecate `tryInvoke`. We should also create a new lint rule against future usage of `tryInvoke`

RFC to deprecate `tryInvoke`:
https://github.com/emberjs/rfcs/pull/673

Pull request:
Add new rule `no-try-invoke`
Fixes #972

Patch:
diff --git a/README.md b/README.md
index 55436bb2..19e8f05c 100644
--- a/README.md
+++ b/README.md
@@ -124,6 +124,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 | :white_check_mark::wrench: | [no-get-with-default](./docs/rules/no-get-with-default.md) | disallow usage of the Ember's `getWithDefault` function |
 | :white_check_mark::wrench: | [no-get](./docs/rules/no-get.md) | require using ES5 getters instead of Ember's `get` / `getProperties` functions |
 |  | [no-proxies](./docs/rules/no-proxies.md) | disallow using array or object proxies |
+|  | [no-try-invoke](./docs/rules/no-try-invoke.md) | disallow usage of the Ember's `tryInvoke` util |
 | :white_check_mark::wrench: | [require-super-in-init](./docs/rules/require-super-in-init.md) | require super to be called in lifecycle hooks |
 | :wrench: | [use-ember-get-and-set](./docs/rules/use-ember-get-and-set.md) | enforce usage of `Ember.get` and `Ember.set` |
 
diff --git a/docs/rules/no-try-invoke.md b/docs/rules/no-try-invoke.md
new file mode 100644
index 00000000..f39e36a4
--- /dev/null
+++ b/docs/rules/no-try-invoke.md
@@ -0,0 +1,36 @@
+# no-try-invoke
+
+This rule will catch and prevent the use of `tryInvoke`.
+
+## Rule Details
+
+This rule aims to disallow the usage of `tryInvoke`. Native JavaScript language now supports [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) and developers are encouraged to use optional chaining `?.()` instead.
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```js
+import { tryInvoke } from '@ember/utils';
+
+class FooComponent extends Component {
+  foo() {
+    tryInvoke(this.args, 'bar', ['baz']);
+  }
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+class FooComponent extends Component {
+  foo() {
+    this.args.bar?.('baz');
+  }
+}
+```
+
+## References
+
+- [RFC](https://github.com/emberjs/rfcs/pull/673) to deprecate `tryInvoke`
+- [spec](https://api.emberjs.com/ember/release/functions/@ember%2Futils/tryInvoke)
diff --git a/lib/index.js b/lib/index.js
index eb76f9b5..25af9fff 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -58,6 +58,7 @@ module.exports = {
     'no-test-module-for': require('./rules/no-test-module-for'),
     'no-test-support-import': require('./rules/no-test-support-import'),
     'no-test-this-render': require('./rules/no-test-this-render'),
+    'no-try-invoke': require('./rules/no-try-invoke'),
     'no-unnecessary-index-route': require('./rules/no-unnecessary-index-route'),
     'no-unnecessary-route-path-option': require('./rules/no-unnecessary-route-path-option'),
     'no-unnecessary-service-injection-argument': require('./rules/no-unnecessary-service-injection-argument'),
diff --git a/lib/rules/no-try-invoke.js b/lib/rules/no-try-invoke.js
new file mode 100644
index 00000000..beafe336
--- /dev/null
+++ b/lib/rules/no-try-invoke.js
@@ -0,0 +1,46 @@
+'use strict';
+
+const types = require('../utils/types');
+const { getImportIdentifier } = require('../utils/import');
+
+const ERROR_MESSAGE = 'Use optional chaining operator `?.()` instead of `tryInvoke`';
+
+module.exports = {
+  ERROR_MESSAGE,
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: "disallow usage of the Ember's `tryInvoke` util",
+      category: 'Ember Object',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-try-invoke.md',
+    },
+    fixable: null,
+    schema: [],
+  },
+
+  create(context) {
+    let importedTryInvokeName;
+
+    return {
+      ImportDeclaration(node) {
+        const importSource = node.source.value;
+
+        if (importSource === '@ember/utils') {
+          importedTryInvokeName =
+            importedTryInvokeName || getImportIdentifier(node, '@ember/utils', 'tryInvoke');
+        }
+      },
+
+      CallExpression(node) {
+        if (types.isIdentifier(node.callee) && node.callee.name === importedTryInvokeName) {
+          context.report({
+            node,
+            message: ERROR_MESSAGE,
+          });
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/no-try-invoke.js b/tests/lib/rules/no-try-invoke.js
new file mode 100644
index 00000000..7531be95
--- /dev/null
+++ b/tests/lib/rules/no-try-invoke.js
@@ -0,0 +1,47 @@
+const rule = require('../../../lib/rules/no-try-invoke');
+const RuleTester = require('eslint').RuleTester;
+
+const { ERROR_MESSAGE } = rule;
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run('no-try-invoke', rule, {
+  valid: [
+    "tryInvoke(this, 'foo');",
+    "import { tryInvoke } from '@ember/utils'; foo.tryInvoke(this, 'foo');",
+    "import { tryInvoke } from '@ember/utils'; tryInvoke.foo(this, 'foo');",
+    "import { tryInvoke } from '@ember/utils'; foo();",
+  ],
+  invalid: [
+    {
+      code: `
+        import { tryInvoke } from '@ember/utils';
+        tryInvoke(this, 'foo');
+      `,
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE,
+          type: 'CallExpression',
+        },
+      ],
+    },
+    {
+      code: `
+        import { tryInvoke, isPresent } from '@ember/utils';
+        tryInvoke(this, 'foo', ['bar']);
+      `,
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE,
+          type: 'CallExpression',
+        },
+      ],
+    },
+  ],
+});