Skip to content

Commit 6788d86

Browse files
Mesteerysindresorhus
andauthoredNov 2, 2023
Add no-unnecessary-polyfills rule (#1717)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 2f77a23 commit 6788d86

File tree

6 files changed

+446
-0
lines changed

6 files changed

+446
-0
lines changed
 

‎configs/recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454
'unicorn/no-this-assignment': 'error',
5555
'unicorn/no-typeof-undefined': 'error',
5656
'unicorn/no-unnecessary-await': 'error',
57+
'unicorn/no-unnecessary-polyfills': 'error',
5758
'unicorn/no-unreadable-array-destructuring': 'error',
5859
'unicorn/no-unreadable-iife': 'error',
5960
'unicorn/no-unused-properties': 'off',
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Enforce the use of built-in methods instead of unnecessary polyfills
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
8+
<!-- RULE_NOTICE -->
9+
10+
_This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config._
11+
12+
<!-- /RULE_NOTICE -->
13+
14+
This rules helps to use existing methods instead of using extra polyfills.
15+
16+
## Fail
17+
18+
package.json
19+
20+
```json
21+
{
22+
"engines": {
23+
"node": ">=8"
24+
}
25+
}
26+
```
27+
28+
```js
29+
const assign = require('object-assign');
30+
```
31+
32+
## Pass
33+
34+
package.json
35+
36+
```json
37+
{
38+
"engines": {
39+
"node": "4"
40+
}
41+
}
42+
```
43+
44+
```js
45+
const assign = require('object-assign'); // Passes as Object.assign is not supported
46+
```
47+
48+
## Options
49+
50+
Type: `object`
51+
52+
### targets
53+
54+
Type: `string | string[] | object`
55+
56+
Specify the target versions, which could be a Browserlist query or a targets object. See the [core-js-compat `targets` option](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js-compat#targets-option) for more info.
57+
58+
If the option is not specified, the target versions are defined using the [`browserlist`](https://browsersl.ist) field in package.json, or as a last resort, the `engines` field in package.json.
59+
60+
```js
61+
"unicorn/no-unnecessary-polyfills": [
62+
"error",
63+
{
64+
"targets": "node >=12"
65+
}
66+
]
67+
```
68+
69+
```js
70+
"unicorn/no-unnecessary-polyfills": [
71+
"error",
72+
{
73+
"targets": [
74+
"node 14.1.0",
75+
"chrome 95"
76+
]
77+
}
78+
]
79+
```
80+
81+
```js
82+
"unicorn/no-unnecessary-polyfills": [
83+
"error",
84+
{
85+
"targets": {
86+
"node": "current",
87+
"firefox": "15"
88+
}
89+
}
90+
]
91+
```

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@eslint-community/eslint-utils": "^4.4.0",
5252
"ci-info": "^3.8.0",
5353
"clean-regexp": "^1.0.0",
54+
"core-js-compat": "^3.33.2",
5455
"esquery": "^1.5.0",
5556
"indent-string": "^4.0.0",
5657
"is-builtin-module": "^3.2.1",

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
9696
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. || | |
9797
| [no-typeof-undefined](docs/rules/no-typeof-undefined.md) | Disallow comparing `undefined` using `typeof`. || 🔧 | 💡 |
9898
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. || 🔧 | |
99+
| [no-unnecessary-polyfills](docs/rules/no-unnecessary-polyfills.md) | Enforce the use of built-in methods instead of unnecessary polyfills. || | |
99100
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. || 🔧 | |
100101
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. || | |
101102
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |

‎rules/no-unnecessary-polyfills.js

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
'use strict';
2+
const path = require('node:path');
3+
const readPkgUp = require('read-pkg-up');
4+
const coreJsCompat = require('core-js-compat');
5+
const {camelCase} = require('lodash');
6+
const isStaticRequire = require('./ast/is-static-require.js');
7+
8+
const {data: compatData, entries: coreJsEntries} = coreJsCompat;
9+
10+
const MESSAGE_ID_POLYFILL = 'unnecessaryPolyfill';
11+
const MESSAGE_ID_CORE_JS = 'unnecessaryCoreJsModule';
12+
const messages = {
13+
[MESSAGE_ID_POLYFILL]: 'Use built-in instead.',
14+
[MESSAGE_ID_CORE_JS]:
15+
'All polyfilled features imported from `{{coreJsModule}}` are available as built-ins. Use the built-ins instead.',
16+
};
17+
18+
const additionalPolyfillPatterns = {
19+
'es.promise.finally': '|(p-finally)',
20+
'es.object.set-prototype-of': '|(setprototypeof)',
21+
'es.string.code-point-at': '|(code-point-at)',
22+
};
23+
24+
const prefixes = '(mdn-polyfills/|polyfill-)';
25+
const suffixes = '(-polyfill)';
26+
const delimiter = '(\\.|-|\\.prototype\\.|/)?';
27+
28+
const polyfills = Object.keys(compatData).map(feature => {
29+
let [ecmaVersion, constructorName, methodName = ''] = feature.split('.');
30+
31+
if (ecmaVersion === 'es') {
32+
ecmaVersion = '(es\\d*)';
33+
}
34+
35+
constructorName = `(${constructorName}|${camelCase(constructorName)})`;
36+
methodName &&= `(${methodName}|${camelCase(methodName)})`;
37+
38+
const methodOrConstructor = methodName || constructorName;
39+
40+
const patterns = [
41+
`^((${prefixes}?(`,
42+
methodName && `(${ecmaVersion}${delimiter}${constructorName}${delimiter}${methodName})|`, // Ex: es6-array-copy-within
43+
methodName && `(${constructorName}${delimiter}${methodName})|`, // Ex: array-copy-within
44+
`(${ecmaVersion}${delimiter}${constructorName}))`, // Ex: es6-array
45+
`${suffixes}?)|`,
46+
`(${prefixes}${methodOrConstructor}|${methodOrConstructor}${suffixes})`, // Ex: polyfill-copy-within / polyfill-promise
47+
`${additionalPolyfillPatterns[feature] || ''})$`,
48+
];
49+
50+
return {
51+
feature,
52+
pattern: new RegExp(patterns.join(''), 'i'),
53+
};
54+
});
55+
56+
function getTargets(options, dirname) {
57+
if (options?.targets) {
58+
return options.targets;
59+
}
60+
61+
/** @type {readPkgUp.ReadResult | undefined} */
62+
let packageResult;
63+
try {
64+
// It can fail if, for example, the package.json file has comments.
65+
packageResult = readPkgUp.sync({normalize: false, cwd: dirname});
66+
} catch {}
67+
68+
if (!packageResult) {
69+
return;
70+
}
71+
72+
const {browserlist, engines} = packageResult.packageJson;
73+
return browserlist ?? engines;
74+
}
75+
76+
function create(context) {
77+
const targets = getTargets(context.options[0], path.dirname(context.filename));
78+
if (!targets) {
79+
return {};
80+
}
81+
82+
let unavailableFeatures;
83+
try {
84+
unavailableFeatures = coreJsCompat({targets}).list;
85+
} catch {
86+
// This can happen if the targets are invalid or use unsupported syntax like `{node:'*'}`.
87+
return {};
88+
}
89+
90+
const checkFeatures = features => !features.every(feature => unavailableFeatures.includes(feature));
91+
92+
return {
93+
Literal(node) {
94+
if (
95+
!(
96+
(['ImportDeclaration', 'ImportExpression'].includes(node.parent.type) && node.parent.source === node)
97+
|| (isStaticRequire(node.parent) && node.parent.arguments[0] === node)
98+
)
99+
) {
100+
return;
101+
}
102+
103+
const importedModule = node.value;
104+
if (typeof importedModule !== 'string' || ['.', '/'].includes(importedModule[0])) {
105+
return;
106+
}
107+
108+
const coreJsModuleFeatures = coreJsEntries[importedModule.replace('core-js-pure', 'core-js')];
109+
110+
if (coreJsModuleFeatures) {
111+
if (coreJsModuleFeatures.length > 1) {
112+
if (checkFeatures(coreJsModuleFeatures)) {
113+
return {
114+
node,
115+
messageId: MESSAGE_ID_CORE_JS,
116+
data: {
117+
coreJsModule: importedModule,
118+
},
119+
};
120+
}
121+
} else if (!unavailableFeatures.includes(coreJsModuleFeatures[0])) {
122+
return {node, messageId: MESSAGE_ID_POLYFILL};
123+
}
124+
125+
return;
126+
}
127+
128+
const polyfill = polyfills.find(({pattern}) => pattern.test(importedModule));
129+
if (polyfill) {
130+
const [, namespace, method = ''] = polyfill.feature.split('.');
131+
const [, features] = Object.entries(coreJsEntries).find(
132+
entry => entry[0] === `core-js/full/${namespace}${method && '/'}${method}`,
133+
);
134+
if (checkFeatures(features)) {
135+
return {node, messageId: MESSAGE_ID_POLYFILL};
136+
}
137+
}
138+
},
139+
};
140+
}
141+
142+
const schema = [
143+
{
144+
type: 'object',
145+
additionalProperties: false,
146+
required: ['targets'],
147+
properties: {
148+
targets: {
149+
oneOf: [
150+
{
151+
type: 'string',
152+
},
153+
{
154+
type: 'array',
155+
},
156+
{
157+
type: 'object',
158+
},
159+
],
160+
},
161+
},
162+
},
163+
];
164+
165+
/** @type {import('eslint').Rule.RuleModule} */
166+
module.exports = {
167+
create,
168+
meta: {
169+
type: 'suggestion',
170+
docs: {
171+
description: 'Enforce the use of built-in methods instead of unnecessary polyfills.',
172+
},
173+
schema,
174+
messages,
175+
},
176+
};

‎test/no-unnecessary-polyfills.mjs

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import {getTester} from './utils/test.mjs';
2+
3+
const {test} = getTester(import.meta);
4+
5+
test({
6+
valid: [
7+
{
8+
code: 'require("object-assign")',
9+
options: [{targets: {node: '0.1.0'}}],
10+
},
11+
{
12+
code: 'require("this-is-not-a-polyfill")',
13+
options: [{targets: {node: '0.1.0'}}],
14+
},
15+
{
16+
code: 'import assign from "object-assign"',
17+
options: [{targets: {node: '0.1.0'}}],
18+
},
19+
{
20+
code: 'import("object-assign")',
21+
options: [{targets: {node: '0.1.0'}}],
22+
},
23+
{
24+
code: 'require("object-assign")',
25+
options: [{targets: 'node <4'}],
26+
},
27+
{
28+
code: 'require("object-assign")',
29+
options: [{targets: 'node >3'}],
30+
},
31+
{
32+
code: 'require()',
33+
options: [{targets: 'node >3'}],
34+
},
35+
{
36+
code: 'import("")',
37+
options: [{targets: 'node >3'}],
38+
},
39+
{
40+
code: 'import(null)',
41+
options: [{targets: 'node >3'}],
42+
},
43+
{
44+
code: 'require(null)',
45+
options: [{targets: 'node >3'}],
46+
},
47+
{
48+
code: 'require("" )',
49+
options: [{targets: 'node >3'}],
50+
},
51+
],
52+
invalid: [
53+
{
54+
code: 'require("setprototypeof")',
55+
options: [{targets: 'node >4'}],
56+
errors: [{message: 'Use built-in instead.'}],
57+
},
58+
{
59+
code: 'require("core-js/features/array/last-index-of")',
60+
options: [{targets: 'node >6.5'}],
61+
errors: [{message: 'Use built-in instead.'}],
62+
},
63+
{
64+
code: 'require("core-js-pure/features/array/from")',
65+
options: [{targets: 'node >7'}],
66+
errors: [{message: 'All polyfilled features imported from `core-js-pure/features/array/from` are available as built-ins. Use the built-ins instead.'}],
67+
},
68+
{
69+
code: 'require("core-js/features/array/from")',
70+
options: [{targets: 'node >7'}],
71+
errors: [{message: 'All polyfilled features imported from `core-js/features/array/from` are available as built-ins. Use the built-ins instead.'}],
72+
},
73+
{
74+
code: 'require("core-js/features/typed-array")',
75+
options: [{targets: 'node >16'}],
76+
errors: [{message: 'All polyfilled features imported from `core-js/features/typed-array` are available as built-ins. Use the built-ins instead.'}],
77+
},
78+
{
79+
code: 'require("es6-symbol")',
80+
options: [{targets: 'node >15'}],
81+
errors: [{message: 'Use built-in instead.'}],
82+
},
83+
{
84+
code: 'require("code-point-at")',
85+
options: [{targets: 'node >4'}],
86+
errors: [{message: 'Use built-in instead.'}],
87+
},
88+
{
89+
code: 'require("object.getownpropertydescriptors")',
90+
options: [{targets: 'node >8'}],
91+
errors: [{message: 'Use built-in instead.'}],
92+
},
93+
{
94+
code: 'require("string.prototype.padstart")',
95+
options: [{targets: 'node >8'}],
96+
errors: [{message: 'Use built-in instead.'}],
97+
98+
},
99+
{
100+
code: 'require("p-finally")',
101+
options: [{targets: 'node >10.4'}],
102+
errors: [{message: 'Use built-in instead.'}],
103+
104+
},
105+
{
106+
code: 'require("promise-polyfill")',
107+
options: [{targets: 'node >15'}],
108+
errors: [{message: 'Use built-in instead.'}],
109+
},
110+
{
111+
code: 'require("es6-promise")',
112+
options: [{targets: 'node >15'}],
113+
errors: [{message: 'Use built-in instead.'}],
114+
},
115+
{
116+
code: 'require("object-assign")',
117+
options: [{targets: 'node 6'}],
118+
errors: [{message: 'Use built-in instead.'}],
119+
},
120+
{
121+
code: 'import assign from "object-assign"',
122+
options: [{targets: 'node 6'}],
123+
errors: [{message: 'Use built-in instead.'}],
124+
},
125+
{
126+
code: 'import("object-assign")',
127+
options: [{targets: 'node 6'}],
128+
errors: [{message: 'Use built-in instead.'}],
129+
},
130+
{
131+
code: 'require("object-assign")',
132+
options: [{targets: 'node >6'}],
133+
errors: [{message: 'Use built-in instead.'}],
134+
},
135+
{
136+
code: 'require("object-assign")',
137+
options: [{targets: 'node 8'}],
138+
errors: [{message: 'Use built-in instead.'}],
139+
},
140+
{
141+
code: 'require("array-from")',
142+
options: [{targets: 'node >7'}],
143+
errors: [{message: 'Use built-in instead.'}],
144+
},
145+
{
146+
code: 'require("array-find-index")',
147+
options: [{targets: 'node >4.0.0'}],
148+
errors: [{message: 'Use built-in instead.'}],
149+
},
150+
{
151+
code: 'require("array-find-index")',
152+
options: [{targets: 'node >4'}],
153+
errors: [{message: 'Use built-in instead.'}],
154+
},
155+
{
156+
code: 'require("array-find-index")',
157+
options: [{targets: 'node 4'}],
158+
errors: [{message: 'Use built-in instead.'}],
159+
},
160+
{
161+
code: 'require("mdn-polyfills/Array.prototype.findIndex")',
162+
options: [{targets: 'node 4'}],
163+
errors: [{message: 'Use built-in instead.'}],
164+
},
165+
{
166+
code: 'require("weakmap-polyfill")',
167+
options: [{targets: 'node 12'}],
168+
errors: [{message: 'Use built-in instead.'}],
169+
},
170+
{
171+
code: 'require("typed-array-float64-array-polyfill")',
172+
options: [{targets: 'node 17'}],
173+
errors: [{message: 'Use built-in instead.'}],
174+
},
175+
],
176+
});

0 commit comments

Comments
 (0)
Please sign in to comment.