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

Issue:
False positives with `no-array-prototype-extensions` rule
[no-array-prototype-extensions](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/no-array-prototype-extensions.md)

False positives with these common non-array classes:

* `window.localStorage.clear();`
* `Promise.reject();`
* `Promise.reject('foo');`

Please comment on this issue with any other examples you notice. 

Potential mitigations:

* We could assume capitalized variables like `Promise` are not arrays as they are more-likely to be classes based on naming convention
* We could ignore known globals like `window` / known variables like `window.localStorage`

CC: @smilland 

Pull request description:
Ignore some commonly-known non-array functions/objects to reduce false positives in `no-array-prototype-extensions` rule
Fixes #1534.

CC: @smilland 

I wrote a rough script to find known non-array functions that could overlap with Ember array prototype extensions. I ran this script on the global `window` object, lodash, and jQuery to find overlaps.

```js
const EXTENSION_METHODS = [/* from rule implementation */];

const detectedMethods = [];

function listMethods(obj, name) {
  if (!obj) {
    return;
  }

  for (const item of [...Object.keys(obj.__proto__ || {}), ...Object.keys(obj)]) {
    if (
      [
        'window',
        'self',
        'frames',
        'top',
        'parent',
        'delegate',
        'dispatcher',
        '__wizmanager',
        'enabledPlugin',
      ].includes(item) ||
      !obj[item]
    ) {
      // avoid stack overflow
      continue;
    }

    // Check for function names.
    if (!item.startsWith('_') && typeof obj[item] === 'function') {
      detectedMethods.push(name ? `${name}.${obj[item].name}` : obj[item].name);
    }

    // Check for objects to recurse into.
    if (typeof obj[item] === 'object' && !Array.isArray(obj[item])) {
      listMethods(obj[item], name ? `${name}.${item}` : item);
    }
  }
}

// Search starting with global window object.
listMethods(window);

// Print results.
console.log('ALL FUNCTIONS DETECTED');
console.log(detectedMethods);
console.log('FUNCTIONS OVERLAPPING WITH ARRAY EXTENSIONS');
console.log(
  detectedMethods.filter((item) => EXTENSION_METHODS.some((ext) => item.endsWith(`.${ext}`)))
);
```

Patch:
diff --git a/lib/rules/no-array-prototype-extensions.js b/lib/rules/no-array-prototype-extensions.js
index a6ebd635..832e3b17 100644
--- a/lib/rules/no-array-prototype-extensions.js
+++ b/lib/rules/no-array-prototype-extensions.js
@@ -1,5 +1,7 @@
 'use strict';
 
+const { getName } = require('../utils/utils');
+
 const ERROR_MESSAGE = "Don't use Ember's array prototype extensions";
 
 const EXTENSION_METHODS = new Set([
@@ -54,6 +56,30 @@ const REPLACE_METHOD = 'replace';
  * EmberArray properties excluding native props: [], length.
  * */
 const EXTENSION_PROPERTIES = new Set(['lastObject', 'firstObject']);
+
+const KNOWN_NON_ARRAY_FUNCTION_CALLS = new Set([
+  // Promise.reject()
+  'window.Promise.reject',
+  'Promise.reject',
+
+  // *storage.clear()
+  'window.localStorage.clear',
+  'window.sessionStorage.clear',
+  'localStorage.clear',
+  'sessionStorage.clear',
+]);
+
+const KNOWN_NON_ARRAY_OBJECTS = new Set([
+  // lodash
+  '_',
+  'lodash',
+
+  // jquery
+  '$',
+  'jQuery',
+  'jquery',
+]);
+
 //----------------------------------------------------------------------------------------------
 // General rule - Don't use Ember's array prototype extensions like .any(), .pushObject() or .firstObject
 //----------------------------------------------------------------------------------------------
@@ -97,6 +123,15 @@ module.exports = {
           return;
         }
 
+        const name = getName(node);
+        if (
+          KNOWN_NON_ARRAY_FUNCTION_CALLS.has(name) ||
+          KNOWN_NON_ARRAY_OBJECTS.has(name.split('.')[0])
+        ) {
+          // Ignore any known non-array objects/function calls to reduce false positives.
+          return;
+        }
+
         if (EXTENSION_METHODS.has(node.callee.property.name)) {
           context.report({ node, messageId: 'main' });
         }


Test patch:
diff --git a/tests/lib/rules/no-array-prototype-extensions.js b/tests/lib/rules/no-array-prototype-extensions.js
index 2f00d9b4..1cece71f 100644
--- a/tests/lib/rules/no-array-prototype-extensions.js
+++ b/tests/lib/rules/no-array-prototype-extensions.js
@@ -42,6 +42,38 @@ ruleTester.run('no-array-prototype-extensions', rule, {
     /** String.prototype.replace() */
     "'something'.replace(regexp, 'substring')",
     "something.replace(regexp, 'substring')",
+
+    // Global non-array class (Promise.reject)
+    'window.Promise.reject();',
+    'Promise.reject();',
+    'Promise.reject("some reason");',
+    'reject();',
+
+    // Global non-array class (*storage.clear)
+    'window.localStorage.clear();',
+    'window.sessionStorage.clear();',
+    'localStorage.clear();',
+    'sessionStorage.clear();',
+    'clear();',
+
+    // Global non-array class (location.replace)
+    'window.document.location.replace(url)',
+    'document.location.replace(url)',
+    'location.replace(url)',
+
+    // Lodash utility functions
+    "lodash.compact([0, 1, false, 2, '', 3]);",
+    "_.compact([0, 1, false, 2, '', 3]);",
+    '_.reject(users, function(o) { return !o.active; });',
+    "_.toArray({ 'a': 1, 'b': 2 });",
+    '_.uniq([2, 1, 2]);',
+    '_.uniqBy([2.1, 1.2, 2.3], Math.floor);',
+    "_.replace('Hi Fred', 'Fred', 'Barney');",
+
+    // jQuery
+    '$( "li" ).toArray();',
+    'jQuery( "li" ).toArray();',
+    'jquery( "li" ).toArray();',
   ],
   invalid: [
     {