Skip to content

Commit 3b61fc1

Browse files
committed
Add no-unnecessary-polyfills rule
Fixes: #36
1 parent 9de8a44 commit 3b61fc1

File tree

6 files changed

+482
-0
lines changed

6 files changed

+482
-0
lines changed

configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = {
5151
'unicorn/no-static-only-class': 'error',
5252
'unicorn/no-thenable': 'error',
5353
'unicorn/no-this-assignment': 'error',
54+
'unicorn/no-unnecessary-polyfills': 'error',
5455
'unicorn/no-unreadable-array-destructuring': 'error',
5556
'unicorn/no-unsafe-regex': 'off',
5657
'unicorn/no-unused-properties': 'off',
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Enforce the use of built-in methods instead of unnecessary polyfills
2+
3+
<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
4+
<!-- RULE_NOTICE -->
5+
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
6+
<!-- /RULE_NOTICE -->
7+
8+
This rules helps to use existing methods instead of using extra polyfills.
9+
10+
## Fail
11+
12+
package.json
13+
14+
```json
15+
{
16+
"engines": {
17+
"node": ">=8"
18+
}
19+
}
20+
```
21+
22+
```js
23+
const assign = require('object-assign');
24+
```
25+
26+
## Pass
27+
28+
package.json
29+
30+
```json
31+
{
32+
"engines": {
33+
"node": "4"
34+
}
35+
}
36+
```
37+
38+
```js
39+
const assign = require('object-assign'); // passes as Object.assign is not supported
40+
```
41+
42+
## Options
43+
44+
Type: `object`
45+
46+
### targets
47+
48+
Type: `string | string[] | object`
49+
50+
The `targets` option allows to specify the target versions. This option could be a Browserlist query or a targets object, see [core-js-compat `targets` option](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js-compat#targets-option) for more informations. It could also be a Node.js target version (SemVer syntax supported) if the [`treatsTargetsAsSemver` option](#treatsTargetsAsSemver) is set to `true`.
51+
52+
If the option is unspecified, the targets are taken from the `package.json`, from the `engines.node` field (as SemVer Node.js target version), or alternatively, from the `browserlist` field (as Browserlist query). This logic can be reversed by setting the [`useBrowserlistFieldByDefault` option](#useBrowserlistFieldByDefault) to `true`.
53+
54+
```js
55+
"unicorn/no-unnecessary-polyfills": ["error", { "targets": "node >=12" }]
56+
```
57+
58+
```js
59+
"unicorn/no-unnecessary-polyfills": ["error", { "targets": ["node 14.1.0", "chrome 95"] }]
60+
```
61+
62+
```js
63+
"unicorn/no-unnecessary-polyfills": ["error", { "targets": { "node": "current", "firefox": "15" } }]
64+
```
65+
66+
### treatsTargetsAsSemver
67+
68+
Type: `boolean`
69+
70+
By default, the `targets` option is treated as a Browserlist query or a targets object. If you want to treat it as a Node.js target version (SemVer syntax supported), set this option to `true`.
71+
72+
### useBrowserlistFieldByDefault
73+
74+
Type: `boolean`
75+
76+
When the `targets` option is not specified, the Node.js target version is taken from the `package.json` file, in `engines.node` field, and fallback to Browserlist targets, in `browserlist` field. If this option is set to `true`, the logic is reversed: the targets will be taken from the `browserlist` field, and fallback to `engines.node` field.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@babel/helper-validator-identifier": "^7.15.7",
5050
"ci-info": "^3.3.0",
5151
"clean-regexp": "^1.0.0",
52+
"core-js-compat": "^3.21.0",
5253
"eslint-utils": "^3.0.0",
5354
"esquery": "^1.4.0",
5455
"indent-string": "^4.0.0",

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Each rule has emojis denoting:
9292
| [no-static-only-class](docs/rules/no-static-only-class.md) | Forbid classes that only have static members. || 🔧 | |
9393
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. || | |
9494
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. || | |
95+
| [no-unnecessary-polyfills](docs/rules/no-unnecessary-polyfills.md) | Enforce the use of built-in methods instead of unnecessary polyfills. || | |
9596
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. || 🔧 | |
9697
| [no-unsafe-regex](docs/rules/no-unsafe-regex.md) | Disallow unsafe regular expressions. | | | |
9798
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |

rules/no-unnecessary-polyfills.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
'use strict';
2+
const semver = require('semver');
3+
const readPkgUp = require('read-pkg-up');
4+
const coreJsCompat = require('core-js-compat');
5+
const {camelCase, upperFirst} = require('lodash');
6+
7+
const {data: compatData, entries: coreJsEntries} = coreJsCompat;
8+
9+
const MESSAGE_ID_POLYFILL = 'unnecessaryPolyfill';
10+
const MESSAGE_ID_CORE_JS = 'unnecessaryCoreJsModule';
11+
const messages = {
12+
[MESSAGE_ID_POLYFILL]: 'Use the built-in `{{featureName}}`.',
13+
[MESSAGE_ID_CORE_JS]: 'All polyfilled features imported from `{{coreJsModule}}` are disponible as built-ins. Use the built-ins instead.',
14+
};
15+
16+
function getTargetsFromPkg(cwd, useBrowserlistFieldByDefault) {
17+
const result = readPkgUp.sync({cwd});
18+
if (!result || !result.pkg) {
19+
return;
20+
}
21+
22+
const {browserlist} = result.pkg;
23+
const nodeEngine = result.pkg.engines && result.pkg.engines.node && new SemverNodeVersion(result.pkg.engines.node);
24+
return useBrowserlistFieldByDefault ? browserlist || nodeEngine : nodeEngine || browserlist;
25+
}
26+
27+
const constructorCaseExceptions = {
28+
regexp: 'RegExp',
29+
};
30+
function constructorCase(name) {
31+
return constructorCaseExceptions[name] || upperFirst(camelCase(name));
32+
}
33+
34+
const additionalPolyfillPatterns = {
35+
'es.promise.finally': '|(p-finally)',
36+
'es.object.set-prototype-of': '|(setprototypeof)',
37+
'es.string.code-point-at': '|(code-point-at)',
38+
};
39+
40+
const prefixes = '(mdn-polyfills|polyfill-)';
41+
const suffixes = '(-polyfill)';
42+
const delimiter = '(\\.|-|\\.prototype\\.|/)?';
43+
44+
const polyfills = Object.keys(compatData).map(feature => {
45+
let [ecmaVersion, constructorName, methodName = ''] = feature.split('.');
46+
47+
if (ecmaVersion === 'es') {
48+
ecmaVersion = `(${ecmaVersion}\\d*)`;
49+
}
50+
51+
constructorName = `(${constructorName}|${camelCase(constructorName)})`;
52+
if (methodName) {
53+
methodName = `(${methodName}|${camelCase(methodName)})`;
54+
}
55+
56+
const methodOrConstructor = methodName || constructorName;
57+
58+
return {
59+
feature,
60+
pattern: new RegExp(
61+
`^((${prefixes}?`
62+
+ `(${ecmaVersion}${delimiter}${constructorName}${delimiter}${methodName}|` // Ex: es6-array-copy-within
63+
+ `${constructorName}${delimiter}${methodName}|` // Ex: array-copy-within
64+
+ `${ecmaVersion}${delimiter}${constructorName})` // Ex: es6-array
65+
+ `${suffixes}?)|`
66+
+ `(${prefixes}${methodOrConstructor}|${methodOrConstructor}${suffixes})` // Ex: polyfill-copy-within / polyfill-promise
67+
+ `${additionalPolyfillPatterns[feature] || ''})$`,
68+
'i',
69+
),
70+
};
71+
});
72+
73+
function report(context, node, feature) {
74+
let [ecmaVersion, namespace, method = ''] = feature.split('.');
75+
if (namespace === 'typed-array' && method.endsWith('-array')) {
76+
namespace = method;
77+
method = '';
78+
}
79+
80+
const delimiter = method && (ecmaVersion === 'node' ? '.' : '#');
81+
82+
context.report({
83+
node,
84+
messageId: MESSAGE_ID_POLYFILL,
85+
data: {
86+
featureName: `${constructorCase(namespace)}${delimiter}${camelCase(method)}`,
87+
},
88+
});
89+
}
90+
91+
class SemverNodeVersion {
92+
constructor(nodeVersion) {
93+
this.nodeVersion = nodeVersion;
94+
this.validNodeVersion = semver.coerce(nodeVersion);
95+
}
96+
97+
compare(featureVersion) {
98+
const supportedNodeVersion = semver.coerce(featureVersion);
99+
return this.validNodeVersion
100+
? semver.lte(supportedNodeVersion, this.validNodeVersion)
101+
: semver.ltr(supportedNodeVersion, this.nodeVersion);
102+
}
103+
}
104+
105+
function processRule(context, node, moduleName, targets) {
106+
if (!moduleName || typeof moduleName !== 'string') {
107+
return;
108+
}
109+
110+
const nodeVersion = targets[0] instanceof SemverNodeVersion && targets[0];
111+
const importedModule = moduleName.replace(/([/\\].+?)\.[^.]+$/, '$1');
112+
113+
const unavailableFeatures = coreJsCompat({targets}).list;
114+
const coreJsModuleFeatures = coreJsEntries[importedModule.replace('core-js-pure', 'core-js')];
115+
116+
const checkFeatures = features => nodeVersion
117+
? features.every(feature => compatData[feature].node && nodeVersion.compare(compatData[feature].node))
118+
: !features.every(feature => unavailableFeatures.includes(feature));
119+
120+
if (coreJsModuleFeatures) {
121+
if (coreJsModuleFeatures.length === 1) {
122+
if (nodeVersion ? nodeVersion.compare(compatData[coreJsModuleFeatures[0]].node) : !unavailableFeatures.includes(coreJsModuleFeatures[0])) {
123+
report(context, node, coreJsModuleFeatures[0]);
124+
}
125+
} else if (checkFeatures(coreJsModuleFeatures)) {
126+
context.report({
127+
node,
128+
messageId: MESSAGE_ID_CORE_JS,
129+
data: {
130+
coreJsModule: moduleName,
131+
},
132+
});
133+
}
134+
135+
return;
136+
}
137+
138+
const polyfill = polyfills.find(({pattern}) => pattern.test(importedModule));
139+
if (polyfill) {
140+
const [, namespace, method = ''] = polyfill.feature.split('.');
141+
const [, features] = Object.entries(coreJsEntries).find(it => it[0] === `core-js/actual/${namespace}${method && '/'}${method}`);
142+
if (checkFeatures(features)) {
143+
report(context, node, polyfill.feature);
144+
}
145+
}
146+
}
147+
148+
function create(context) {
149+
const options = context.options[0];
150+
let targets = options && options.targets;
151+
if (!targets) {
152+
getTargetsFromPkg(context.getFilename(), options && options.useBrowserlistFieldByDefault);
153+
} else if (options.treatsTargetsAsSemver) {
154+
targets = new SemverNodeVersion(targets);
155+
}
156+
157+
if (!targets) {
158+
return {};
159+
}
160+
161+
return {
162+
'CallExpression[callee.name="require"]'(node) {
163+
processRule(context, node, node.arguments[0].value, targets);
164+
},
165+
'ImportDeclaration, ImportExpression'(node) {
166+
processRule(context, node, node.source.value, targets);
167+
},
168+
};
169+
}
170+
171+
const schema = [
172+
{
173+
type: 'object',
174+
additionalProperties: false,
175+
required: ['targets'],
176+
properties: {
177+
useBrowserlistFieldByDefault: {type: 'boolean'},
178+
treatsTargetsAsSemver: {type: 'boolean'},
179+
targets: {
180+
oneOf: [
181+
{
182+
type: 'string',
183+
minLength: 1,
184+
},
185+
{
186+
type: 'array',
187+
minItems: 1,
188+
items: {
189+
type: 'string',
190+
},
191+
},
192+
{
193+
type: 'object',
194+
minProperties: 1,
195+
properties: {
196+
android: {type: 'string'},
197+
chrome: {type: 'string'},
198+
deno: {type: 'string'},
199+
edge: {type: 'string'},
200+
electron: {type: 'string'},
201+
firefox: {type: 'string'},
202+
ie: {type: 'string'},
203+
ios: {type: 'string'},
204+
node: {type: 'string'},
205+
opera: {type: 'string'},
206+
// eslint-disable-next-line camelcase
207+
opera_mobile: {type: 'string'},
208+
phantom: {type: 'string'},
209+
rhino: {type: 'string'},
210+
safari: {type: 'string'},
211+
samsung: {type: 'string'},
212+
esmodules: {type: 'boolean'},
213+
browsers: {
214+
oneOf: [
215+
{
216+
type: 'string',
217+
minLength: 1,
218+
},
219+
{
220+
type: 'array',
221+
minItems: 1,
222+
items: {
223+
type: 'string',
224+
},
225+
},
226+
{
227+
type: 'object',
228+
minProperties: 1,
229+
},
230+
],
231+
},
232+
},
233+
},
234+
],
235+
},
236+
},
237+
},
238+
];
239+
240+
/** @type {import('eslint').Rule.RuleModule} */
241+
module.exports = {
242+
create,
243+
meta: {
244+
type: 'suggestion',
245+
docs: {
246+
description: 'Enforce the use of built-in methods instead of unnecessary polyfills.',
247+
},
248+
schema,
249+
messages,
250+
},
251+
};

0 commit comments

Comments
 (0)