Skip to content

Commit

Permalink
Add no-unnecessary-polyfills rule (#1717)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Mesteery and sindresorhus authored Nov 2, 2023
1 parent 2f77a23 commit 6788d86
Show file tree
Hide file tree
Showing 6 changed files with 446 additions and 0 deletions.
1 change: 1 addition & 0 deletions configs/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {
'unicorn/no-this-assignment': 'error',
'unicorn/no-typeof-undefined': 'error',
'unicorn/no-unnecessary-await': 'error',
'unicorn/no-unnecessary-polyfills': 'error',
'unicorn/no-unreadable-array-destructuring': 'error',
'unicorn/no-unreadable-iife': 'error',
'unicorn/no-unused-properties': 'off',
Expand Down
91 changes: 91 additions & 0 deletions docs/rules/no-unnecessary-polyfills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Enforce the use of built-in methods instead of unnecessary polyfills

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs).

<!-- end auto-generated rule header -->

<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
<!-- RULE_NOTICE -->

_This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config._

<!-- /RULE_NOTICE -->

This rules helps to use existing methods instead of using extra polyfills.

## Fail

package.json

```json
{
"engines": {
"node": ">=8"
}
}
```

```js
const assign = require('object-assign');
```

## Pass

package.json

```json
{
"engines": {
"node": "4"
}
}
```

```js
const assign = require('object-assign'); // Passes as Object.assign is not supported
```

## Options

Type: `object`

### targets

Type: `string | string[] | object`

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.

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.

```js
"unicorn/no-unnecessary-polyfills": [
"error",
{
"targets": "node >=12"
}
]
```

```js
"unicorn/no-unnecessary-polyfills": [
"error",
{
"targets": [
"node 14.1.0",
"chrome 95"
]
}
]
```

```js
"unicorn/no-unnecessary-polyfills": [
"error",
{
"targets": {
"node": "current",
"firefox": "15"
}
}
]
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@eslint-community/eslint-utils": "^4.4.0",
"ci-info": "^3.8.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.33.2",
"esquery": "^1.5.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. || | |
| [no-typeof-undefined](docs/rules/no-typeof-undefined.md) | Disallow comparing `undefined` using `typeof`. || 🔧 | 💡 |
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. || 🔧 | |
| [no-unnecessary-polyfills](docs/rules/no-unnecessary-polyfills.md) | Enforce the use of built-in methods instead of unnecessary polyfills. || | |
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. || 🔧 | |
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. || | |
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |
Expand Down
176 changes: 176 additions & 0 deletions rules/no-unnecessary-polyfills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use strict';
const path = require('node:path');
const readPkgUp = require('read-pkg-up');
const coreJsCompat = require('core-js-compat');
const {camelCase} = require('lodash');
const isStaticRequire = require('./ast/is-static-require.js');

const {data: compatData, entries: coreJsEntries} = coreJsCompat;

const MESSAGE_ID_POLYFILL = 'unnecessaryPolyfill';
const MESSAGE_ID_CORE_JS = 'unnecessaryCoreJsModule';
const messages = {
[MESSAGE_ID_POLYFILL]: 'Use built-in instead.',
[MESSAGE_ID_CORE_JS]:
'All polyfilled features imported from `{{coreJsModule}}` are available as built-ins. Use the built-ins instead.',
};

const additionalPolyfillPatterns = {
'es.promise.finally': '|(p-finally)',
'es.object.set-prototype-of': '|(setprototypeof)',
'es.string.code-point-at': '|(code-point-at)',
};

const prefixes = '(mdn-polyfills/|polyfill-)';
const suffixes = '(-polyfill)';
const delimiter = '(\\.|-|\\.prototype\\.|/)?';

const polyfills = Object.keys(compatData).map(feature => {
let [ecmaVersion, constructorName, methodName = ''] = feature.split('.');

if (ecmaVersion === 'es') {
ecmaVersion = '(es\\d*)';
}

constructorName = `(${constructorName}|${camelCase(constructorName)})`;
methodName &&= `(${methodName}|${camelCase(methodName)})`;

const methodOrConstructor = methodName || constructorName;

const patterns = [
`^((${prefixes}?(`,
methodName && `(${ecmaVersion}${delimiter}${constructorName}${delimiter}${methodName})|`, // Ex: es6-array-copy-within
methodName && `(${constructorName}${delimiter}${methodName})|`, // Ex: array-copy-within
`(${ecmaVersion}${delimiter}${constructorName}))`, // Ex: es6-array
`${suffixes}?)|`,
`(${prefixes}${methodOrConstructor}|${methodOrConstructor}${suffixes})`, // Ex: polyfill-copy-within / polyfill-promise
`${additionalPolyfillPatterns[feature] || ''})$`,
];

return {
feature,
pattern: new RegExp(patterns.join(''), 'i'),
};
});

function getTargets(options, dirname) {
if (options?.targets) {
return options.targets;
}

/** @type {readPkgUp.ReadResult | undefined} */
let packageResult;
try {
// It can fail if, for example, the package.json file has comments.
packageResult = readPkgUp.sync({normalize: false, cwd: dirname});
} catch {}

if (!packageResult) {
return;
}

const {browserlist, engines} = packageResult.packageJson;
return browserlist ?? engines;
}

function create(context) {
const targets = getTargets(context.options[0], path.dirname(context.filename));
if (!targets) {
return {};
}

let unavailableFeatures;
try {
unavailableFeatures = coreJsCompat({targets}).list;
} catch {
// This can happen if the targets are invalid or use unsupported syntax like `{node:'*'}`.
return {};
}

const checkFeatures = features => !features.every(feature => unavailableFeatures.includes(feature));

return {
Literal(node) {
if (
!(
(['ImportDeclaration', 'ImportExpression'].includes(node.parent.type) && node.parent.source === node)
|| (isStaticRequire(node.parent) && node.parent.arguments[0] === node)
)
) {
return;
}

const importedModule = node.value;
if (typeof importedModule !== 'string' || ['.', '/'].includes(importedModule[0])) {
return;
}

const coreJsModuleFeatures = coreJsEntries[importedModule.replace('core-js-pure', 'core-js')];

if (coreJsModuleFeatures) {
if (coreJsModuleFeatures.length > 1) {
if (checkFeatures(coreJsModuleFeatures)) {
return {
node,
messageId: MESSAGE_ID_CORE_JS,
data: {
coreJsModule: importedModule,
},
};
}
} else if (!unavailableFeatures.includes(coreJsModuleFeatures[0])) {
return {node, messageId: MESSAGE_ID_POLYFILL};
}

return;
}

const polyfill = polyfills.find(({pattern}) => pattern.test(importedModule));
if (polyfill) {
const [, namespace, method = ''] = polyfill.feature.split('.');
const [, features] = Object.entries(coreJsEntries).find(
entry => entry[0] === `core-js/full/${namespace}${method && '/'}${method}`,
);
if (checkFeatures(features)) {
return {node, messageId: MESSAGE_ID_POLYFILL};
}
}
},
};
}

const schema = [
{
type: 'object',
additionalProperties: false,
required: ['targets'],
properties: {
targets: {
oneOf: [
{
type: 'string',
},
{
type: 'array',
},
{
type: 'object',
},
],
},
},
},
];

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce the use of built-in methods instead of unnecessary polyfills.',
},
schema,
messages,
},
};
Loading

0 comments on commit 6788d86

Please sign in to comment.