Repository: sindresorhus/eslint-plugin-unicorn (JavaScript)

Issue:
Rule proposal: `prefer-prototype-methods`
### Fail

```js
[].push.apply(foo, elements);
```

```js
[].slice.call(foo);
```

```js
({}).toString.call(foo);
```

```js
({}).hasOwnProperty.call(foo, 'bar');
```

### Pass

```js
Array.prototype.push.apply(foo, elements);
```

```js
Array.prototype.slice.call(foo);
```

```js
Object.prototype.toString.call(foo);
```

```js
Object.prototype.hasOwnProperty.call(foo, 'bar');
```

Alter name `no-literal-prototype`


Discussion:
Maybe we should also fobid `foo.constructor.prototype.bar.call()`
This is now accepted.
These are all methods, I think better name is `prefer-prototype-methods`.



Patch:
diff --git a/docs/rules/prefer-prototype-methods.md b/docs/rules/prefer-prototype-methods.md
new file mode 100644
index 0000000000..097647f04d
--- /dev/null
+++ b/docs/rules/prefer-prototype-methods.md
@@ -0,0 +1,49 @@
+# Prefer borrowing methods from the prototype instead of methods from an instance.
+
+When “borrowing” a method from a different object (especially generic methods from `Array`), it‘s clearer to get it from the prototype than from an instance.
+
+This rule is fixable when using method from `[]` or `{}`.
+
+## Fail
+
+```js
+const array = [].slice.apply(bar);
+```
+
+```js
+const hasProperty = {}.hasOwnProperty.call(foo, 'property');
+```
+
+```js
+foo.bar.call(baz);
+```
+
+```js
+Reflect.apply([].forEach, arrayLike, [callback]);
+```
+
+## Pass
+
+```js
+const array = Array.prototype.slice.apply(bar);
+```
+
+```js
+const hasProperty = Object.prototype.hasOwnProperty.call(foo, 'property');
+```
+
+```js
+Foo.prototype.bar.call(baz);
+```
+
+```js
+foo.constructor.prototype.bar.call(baz);
+```
+
+```js
+Reflect.apply(Array.prototype.forEach, arrayLike, [callback]);
+```
+
+```js
+const maxValue = Math.max.apply(Math, numbers);
+```
diff --git a/index.js b/index.js
index 1cccd42220..0950aea863 100644
--- a/index.js
+++ b/index.js
@@ -102,6 +102,7 @@ module.exports = {
 				'unicorn/prefer-node-protocol': 'error',
 				'unicorn/prefer-number-properties': 'error',
 				'unicorn/prefer-optional-catch-binding': 'error',
+				'unicorn/prefer-prototype-methods': 'error',
 				'unicorn/prefer-query-selector': 'error',
 				'unicorn/prefer-reflect-apply': 'error',
 				'unicorn/prefer-regexp-test': 'error',
diff --git a/readme.md b/readme.md
index 1bbfc9ee6c..5f3abf2278 100644
--- a/readme.md
+++ b/readme.md
@@ -98,6 +98,7 @@ Configure it in `package.json`.
 			"unicorn/prefer-node-protocol": "error",
 			"unicorn/prefer-number-properties": "error",
 			"unicorn/prefer-optional-catch-binding": "error",
+			"unicorn/prefer-prototype-methods": "error",
 			"unicorn/prefer-query-selector": "error",
 			"unicorn/prefer-reflect-apply": "error",
 			"unicorn/prefer-regexp-test": "error",
@@ -193,6 +194,7 @@ Each rule has emojis denoting:
 | [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | ✅ | 🔧 |
 | [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ | 🔧 |
 | [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 |
+| [prefer-prototype-methods](docs/rules/prefer-prototype-methods.md) | Prefer borrowing methods from the prototype instead of the instance. | ✅ | 🔧 |
 | [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. | ✅ | 🔧 |
 | [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. | ✅ | 🔧 |
 | [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. | ✅ | 🔧 |
diff --git a/rules/prefer-prototype-methods.js b/rules/prefer-prototype-methods.js
new file mode 100644
index 0000000000..1edb59dc9e
--- /dev/null
+++ b/rules/prefer-prototype-methods.js
@@ -0,0 +1,110 @@
+'use strict';
+const getDocumentationUrl = require('./utils/get-documentation-url');
+const methodSelector = require('./utils/method-selector');
+const getPropertyName = require('./utils/get-property-name');
+
+const messages = {
+	'known-constructor-known-method': 'Prefer using `{{constructorName}}.prototype.{{methodName}}`.',
+	'known-constructor-unknown-method': 'Prefer using method from `{{constructorName}}.prototype`.',
+	'unknown-constructor-known-method': 'Prefer using `{{methodName}}` method from the constructor prototype.',
+	'unknown-constructor-unknown-method': 'Prefer using method from the constructor prototype.'
+};
+
+const functionMethodsSelector = [
+	methodSelector({
+		names: ['apply', 'bind', 'call']
+	}),
+	' > ',
+	'.callee',
+	' > ',
+	'.object'
+].join('');
+
+const reflectApplySelector = methodSelector({
+	object: 'Reflect',
+	name: 'apply',
+	min: 1
+});
+
+function getConstructorName(node) {
+	switch (node.type) {
+		case 'ArrayExpression':
+			return 'Array';
+		case 'ObjectExpression':
+			return 'Object';
+		// No default
+	}
+}
+
+function isSafeToFix(node) {
+	switch (node.type) {
+		case 'ArrayExpression':
+			return node.elements.length === 0;
+		case 'ObjectExpression':
+			return node.properties.length === 0;
+		// No default
+	}
+}
+
+/** @param {import('eslint').Rule.RuleContext} context */
+function create(context) {
+	function check(method) {
+		const {type, object} = method;
+		if (
+			type !== 'MemberExpression' ||
+			// Most likely it's a static method of a class
+			(object.type === 'Identifier' && /^[A-Z]/.test(object.name)) ||
+			(
+				object.type === 'MemberExpression' &&
+				!object.computed &&
+				!object.optional &&
+				object.property.type === 'Identifier' &&
+				object.property.name === 'prototype'
+			)
+		) {
+			return;
+		}
+
+		const constructorName = getConstructorName(object);
+		const methodName = getPropertyName(method, context.getScope());
+		const messageId = [
+			constructorName ? 'known' : 'unknown',
+			'constructor',
+			methodName ? 'known' : 'unknown',
+			'method'
+		].join('-');
+
+		const problem = {
+			node: method,
+			messageId,
+			data: {constructorName, methodName: String(methodName)}
+		};
+
+		if (constructorName && isSafeToFix(object)) {
+			problem.fix = fixer => fixer.replaceText(object, `${constructorName}.prototype`);
+		}
+
+		context.report(problem);
+	}
+
+	return {
+		[reflectApplySelector](node) {
+			check(node.arguments[0]);
+		},
+		[functionMethodsSelector]: check
+	};
+}
+
+module.exports = {
+	create,
+	meta: {
+		type: 'suggestion',
+		docs: {
+			description: 'Prefer borrowing methods from the prototype instead of the instance.',
+			url: getDocumentationUrl(__filename)
+		},
+		fixable: 'code',
+		schema: [],
+		messages
+	}
+};


Test patch:
diff --git a/test/prefer-prototype-methods.mjs b/test/prefer-prototype-methods.mjs
new file mode 100644
index 0000000000..32ba8aec09
--- /dev/null
+++ b/test/prefer-prototype-methods.mjs
@@ -0,0 +1,82 @@
+import {getTester} from './utils/test.mjs';
+
+const {test} = getTester(import.meta);
+
+test.snapshot({
+	valid: [
+		'const foo = Array.prototype.push.apply(bar, elements);',
+		'const foo = Array.prototype.slice.call(bar);',
+		'const foo = Object.prototype.toString.call(bar);',
+		'const foo = Object.prototype.hasOwnProperty.call(bar, "property");',
+		'const foo = Object.prototype.propertyIsEnumerable.call(bar, "property");',
+		'Array.prototype.forEach.call(foo, () => {})',
+		'const push = Array.prototype.push.bind(foo)',
+		'const push = [].push',
+		'const {push} = []',
+		'Math.max.apply(null, numbers)',
+		'foo.apply(null, args)',
+		'Reflect.apply(...[].slice)',
+		'Reflect.apply(foo, [].slice)',
+		'Reflect.apply(Math.max, Math, numbers)',
+		'Reflect.apply()',
+		'Reflect["apply"]([].slice, foo, [])',
+		'NotReflect.apply([].slice, foo, [])',
+		'Reflect.notApply([].slice, foo, [])',
+		'Reflect.apply([]?.slice, foo, [])',
+		// This better use `Foo.prototype.bar.call(baz)`, not handled
+		'foo.constructor.prototype.bar.call(baz)'
+	],
+	invalid: [
+		'const foo = [].push.apply(bar, elements);',
+		'const foo = [].slice.call(bar);',
+		'const foo = {}.toString.call(bar);',
+		'const foo = {}.hasOwnProperty.call(bar, "property");',
+		'const foo = {}.propertyIsEnumerable.call(bar, "property");',
+		'[].forEach.call(foo, () => {})',
+		'const push = [].push.bind(foo)',
+		'const foo = bar.method.call(foo)',
+		'const foo = bar[method].call(foo)',
+		'const method = "realMethodName";const foo = bar[method].call(foo)',
+		'const foo = [][method].call(foo)',
+		'const method = "realMethodName";const foo = [][method].call(foo)',
+		'const foo = [1].push.apply(bar, elements);',
+		'const array = Reflect.apply([].slice, foo, [])',
+		'Reflect.apply(foo.bar, baz, [])',
+		// False positive
+		'Array["prototype"].slice.call();',
+		'Array?.prototype.slice.call();',
+		'window.Math.max.apply(null, numbers)'
+	]
+});
+
+test.babel({
+	testerOptions: {
+		env: {es2021: true}
+	},
+	valid: [],
+	invalid: [
+		{
+			code: 'Reflect.apply(foo[Symbol()], baz, [])',
+			errors: [{message: 'Prefer using method from the constructor prototype.'}]
+		},
+		{
+			code: 'Reflect.apply(foo[Symbol("symbol description")], baz, [])',
+			errors: [{message: 'Prefer using method from the constructor prototype.'}]
+		},
+		{
+			code: 'Reflect.apply([][Symbol()], baz, [])',
+			output: 'Reflect.apply(Array.prototype[Symbol()], baz, [])',
+			errors: [{message: 'Prefer using method from `Array.prototype`.'}]
+		},
+		{
+			code: 'Reflect.apply({}[Symbol("symbol description")], baz, [])',
+			output: 'Reflect.apply(Object.prototype[Symbol("symbol description")], baz, [])',
+			errors: [{message: 'Prefer using method from `Object.prototype`.'}]
+		},
+		{
+			code: '[][Symbol.iterator].call(foo)',
+			output: 'Array.prototype[Symbol.iterator].call(foo)',
+			errors: [{message: 'Prefer using `Array.prototype.Symbol(Symbol.iterator)`.'}]
+		}
+	]
+});
diff --git a/test/snapshots/prefer-prototype-methods.mjs.md b/test/snapshots/prefer-prototype-methods.mjs.md
new file mode 100644
index 0000000000..ba90a68232
--- /dev/null
+++ b/test/snapshots/prefer-prototype-methods.mjs.md
@@ -0,0 +1,245 @@
+# Snapshot report for `test/prefer-prototype-methods.mjs`
+
+The actual snapshot is saved in `prefer-prototype-methods.mjs.snap`.
+
+Generated by [AVA](https://avajs.dev).
+
+## Invalid #1
+      1 | const foo = [].push.apply(bar, elements);
+
+> Output
+
+    `␊
+      1 | const foo = Array.prototype.push.apply(bar, elements);␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = [].push.apply(bar, elements);␊
+        |             ^^^^^^^ Prefer using \`Array.prototype.push\`.␊
+    `
+
+## Invalid #2
+      1 | const foo = [].slice.call(bar);
+
+> Output
+
+    `␊
+      1 | const foo = Array.prototype.slice.call(bar);␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = [].slice.call(bar);␊
+        |             ^^^^^^^^ Prefer using \`Array.prototype.slice\`.␊
+    `
+
+## Invalid #3
+      1 | const foo = {}.toString.call(bar);
+
+> Output
+
+    `␊
+      1 | const foo = Object.prototype.toString.call(bar);␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = {}.toString.call(bar);␊
+        |             ^^^^^^^^^^^ Prefer using \`Object.prototype.toString\`.␊
+    `
+
+## Invalid #4
+      1 | const foo = {}.hasOwnProperty.call(bar, "property");
+
+> Output
+
+    `␊
+      1 | const foo = Object.prototype.hasOwnProperty.call(bar, "property");␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = {}.hasOwnProperty.call(bar, "property");␊
+        |             ^^^^^^^^^^^^^^^^^ Prefer using \`Object.prototype.hasOwnProperty\`.␊
+    `
+
+## Invalid #5
+      1 | const foo = {}.propertyIsEnumerable.call(bar, "property");
+
+> Output
+
+    `␊
+      1 | const foo = Object.prototype.propertyIsEnumerable.call(bar, "property");␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = {}.propertyIsEnumerable.call(bar, "property");␊
+        |             ^^^^^^^^^^^^^^^^^^^^^^^ Prefer using \`Object.prototype.propertyIsEnumerable\`.␊
+    `
+
+## Invalid #6
+      1 | [].forEach.call(foo, () => {})
+
+> Output
+
+    `␊
+      1 | Array.prototype.forEach.call(foo, () => {})␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | [].forEach.call(foo, () => {})␊
+        | ^^^^^^^^^^ Prefer using \`Array.prototype.forEach\`.␊
+    `
+
+## Invalid #7
+      1 | const push = [].push.bind(foo)
+
+> Output
+
+    `␊
+      1 | const push = Array.prototype.push.bind(foo)␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const push = [].push.bind(foo)␊
+        |              ^^^^^^^ Prefer using \`Array.prototype.push\`.␊
+    `
+
+## Invalid #8
+      1 | const foo = bar.method.call(foo)
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = bar.method.call(foo)␊
+        |             ^^^^^^^^^^ Prefer using \`method\` method from the constructor prototype.␊
+    `
+
+## Invalid #9
+      1 | const foo = bar[method].call(foo)
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = bar[method].call(foo)␊
+        |             ^^^^^^^^^^^ Prefer using method from the constructor prototype.␊
+    `
+
+## Invalid #10
+      1 | const method = "realMethodName";const foo = bar[method].call(foo)
+
+> Error 1/1
+
+    `␊
+    > 1 | const method = "realMethodName";const foo = bar[method].call(foo)␊
+        |                                             ^^^^^^^^^^^ Prefer using \`realMethodName\` method from the constructor prototype.␊
+    `
+
+## Invalid #11
+      1 | const foo = [][method].call(foo)
+
+> Output
+
+    `␊
+      1 | const foo = Array.prototype[method].call(foo)␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = [][method].call(foo)␊
+        |             ^^^^^^^^^^ Prefer using method from \`Array.prototype\`.␊
+    `
+
+## Invalid #12
+      1 | const method = "realMethodName";const foo = [][method].call(foo)
+
+> Output
+
+    `␊
+      1 | const method = "realMethodName";const foo = Array.prototype[method].call(foo)␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const method = "realMethodName";const foo = [][method].call(foo)␊
+        |                                             ^^^^^^^^^^ Prefer using \`Array.prototype.realMethodName\`.␊
+    `
+
+## Invalid #13
+      1 | const foo = [1].push.apply(bar, elements);
+
+> Error 1/1
+
+    `␊
+    > 1 | const foo = [1].push.apply(bar, elements);␊
+        |             ^^^^^^^^ Prefer using \`Array.prototype.push\`.␊
+    `
+
+## Invalid #14
+      1 | const array = Reflect.apply([].slice, foo, [])
+
+> Output
+
+    `␊
+      1 | const array = Reflect.apply(Array.prototype.slice, foo, [])␊
+    `
+
+> Error 1/1
+
+    `␊
+    > 1 | const array = Reflect.apply([].slice, foo, [])␊
+        |                             ^^^^^^^^ Prefer using \`Array.prototype.slice\`.␊
+    `
+
+## Invalid #15
+      1 | Reflect.apply(foo.bar, baz, [])
+
+> Error 1/1
+
+    `␊
+    > 1 | Reflect.apply(foo.bar, baz, [])␊
+        |               ^^^^^^^ Prefer using \`bar\` method from the constructor prototype.␊
+    `
+
+## Invalid #16
+      1 | Array["prototype"].slice.call();
+
+> Error 1/1
+
+    `␊
+    > 1 | Array["prototype"].slice.call();␊
+        | ^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using \`slice\` method from the constructor prototype.␊
+    `
+
+## Invalid #17
+      1 | Array?.prototype.slice.call();
+
+> Error 1/1
+
+    `␊
+    > 1 | Array?.prototype.slice.call();␊
+        | ^^^^^^^^^^^^^^^^^^^^^^ Prefer using \`slice\` method from the constructor prototype.␊
+    `
+
+## Invalid #18
+      1 | window.Math.max.apply(null, numbers)
+
+> Error 1/1
+
+    `␊
+    > 1 | window.Math.max.apply(null, numbers)␊
+        | ^^^^^^^^^^^^^^^ Prefer using \`max\` method from the constructor prototype.␊
+    `
diff --git a/test/snapshots/prefer-prototype-methods.mjs.snap b/test/snapshots/prefer-prototype-methods.mjs.snap
new file mode 100644
index 0000000000..5ee841af5d
Binary files /dev/null and b/test/snapshots/prefer-prototype-methods.mjs.snap differ
diff --git a/test/utils/test.mjs b/test/utils/test.mjs
index 0bc073b08c..f8f92c3e08 100644
--- a/test/utils/test.mjs
+++ b/test/utils/test.mjs
@@ -115,10 +115,10 @@ function getTester(importMeta) {
 	const filename = url.fileURLToPath(importMeta.url);
 	const ruleId = path.basename(filename, '.mjs');
 	const tester = new Tester(ruleId);
-	const test = tester.runTest.bind(tester);
-	test.typescript = tester.typescript.bind(tester);
-	test.babel = tester.babel.bind(tester);
-	test.snapshot = tester.snapshot.bind(tester);
+	const test = Tester.prototype.runTest.bind(tester);
+	test.typescript = Tester.prototype.typescript.bind(tester);
+	test.babel = Tester.prototype.babel.bind(tester);
+	test.snapshot = Tester.prototype.snapshot.bind(tester);
 
 	return {
 		ruleId,