Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefer-modern-dom-apis rule #362

Merged
merged 14 commits into from Dec 22, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/rules/prefer-modern-dom-apis.md
@@ -0,0 +1,52 @@
# Prefer modern DOM APIs

Enforces the use of:

- `childNode.replaceWith(newNode);` over `parentNode.replaceChild(newNode, oldNode)`;
- `referenceNode.before(newNode);` over `parentNode.insertBefore(newNode, referenceNode);`
- `referenceNode.before("text");` over `referenceNode.insertAdjacentText("beforebegin", "text");`
- `referenceNode.before(newNode);` over `referenceNode.insertAdjacentElement("beforebegin", newNode);`
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved

There are some advantages of using the newer DOM APIs, like:

- traversing to the parent node is not necessary
- appending multiple nodes at once
- both [`DOMString`](https://developer.mozilla.org/en-US/docs/Web/API/DOMString) and [DOM node objects](https://developer.mozilla.org/en-US/docs/Web/API/Element) can be manipulated.

This rule is fixable.

## Fail

```js
foo.replaceChild(baz, bar);

foo.insertBefore(baz, bar);

foo.insertAdjacentText('position', bar);

foo.insertAdjacentElement('position', bar);
```

## Pass

```js
foo.replaceWith(bar);
foo.replaceWith('bar');
foo.replaceWith(bar, 'baz'));

foo.before(bar)
foo.before('bar')
foo.before(bar, 'baz')

foo.prepend(bar)
foo.prepend('bar')
foo.prepend(bar, 'baz')

foo.append(bar)
foo.append('bar')
foo.append(bar, 'baz')

foo.after(bar)
foo.after('bar')
foo.after(bar, 'baz')
```
5 changes: 2 additions & 3 deletions index.js
Expand Up @@ -13,9 +13,7 @@ module.exports = {
ecmaVersion: 2020,
sourceType: 'module'
},
plugins: [
'unicorn'
],
plugins: ['unicorn'],
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved
rules: {
'unicorn/catch-error-name': 'error',
'unicorn/consistent-function-scoping': 'error',
Expand Down Expand Up @@ -46,6 +44,7 @@ module.exports = {
'unicorn/prefer-exponentiation-operator': 'error',
'unicorn/prefer-flat-map': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
'unicorn/prefer-node-append': 'error',
'unicorn/prefer-node-remove': 'error',
'unicorn/prefer-query-selector': 'error',
Expand Down
10 changes: 3 additions & 7 deletions readme.md
Expand Up @@ -8,14 +8,12 @@ You might want to check out [XO](https://github.com/xojs/xo), which includes thi

[**Propose or contribute a new rule ➡**](.github/contributing.md)


mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved
## Install

```
```bash
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved
$ npm install --save-dev eslint eslint-plugin-unicorn
```


## Usage

Configure it in `package.json`.
Expand Down Expand Up @@ -64,6 +62,7 @@ Configure it in `package.json`.
"unicorn/prefer-exponentiation-operator": "error",
"unicorn/prefer-flat-map": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-modern-dom-apis": "error",
"unicorn/prefer-node-append": "error",
"unicorn/prefer-node-remove": "error",
"unicorn/prefer-query-selector": "error",
Expand All @@ -79,7 +78,6 @@ Configure it in `package.json`.
}
```


## Rules

- [catch-error-name](docs/rules/catch-error-name.md) - Enforce a specific parameter name in catch clauses.
Expand Down Expand Up @@ -111,6 +109,7 @@ Configure it in `package.json`.
- [prefer-exponentiation-operator](docs/rules/prefer-exponentiation-operator.md) - Prefer the exponentiation operator over `Math.pow()` *(fixable)*
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
- [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) - Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. *(fixable)*
- [prefer-node-append](docs/rules/prefer-node-append.md) - Prefer `Node#append()` over `Node#appendChild()`. *(fixable)*
- [prefer-node-remove](docs/rules/prefer-node-remove.md) - Prefer `node.remove()` over `parentNode.removeChild(node)` and `parentElement.removeChild(node)`. *(fixable)*
- [prefer-query-selector](docs/rules/prefer-query-selector.md) - Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. *(partly fixable)*
Expand All @@ -122,7 +121,6 @@ Configure it in `package.json`.
- [regex-shorthand](docs/rules/regex-shorthand.md) - Enforce the use of regex shorthands to improve readability. *(fixable)*
- [throw-new-error](docs/rules/throw-new-error.md) - Require `new` when throwing an error. *(fixable)*


## Recommended config

This plugin exports a [`recommended` config](index.js) that enforces good practices.
Expand All @@ -142,7 +140,6 @@ See the [ESLint docs](http://eslint.org/docs/user-guide/configuring#extending-co

**Note**: This config will also enable the correct [parser options](http://eslint.org/docs/user-guide/configuring#specifying-parser-options) and [environment](http://eslint.org/docs/user-guide/configuring#specifying-environments).


## Maintainers

- [Sindre Sorhus](https://github.com/sindresorhus)
Expand All @@ -154,7 +151,6 @@ See the [ESLint docs](http://eslint.org/docs/user-guide/configuring#extending-co

- [Jeroen Engels](https://github.com/jfmengels)


## License

MIT
136 changes: 136 additions & 0 deletions rules/prefer-modern-dom-apis.js
@@ -0,0 +1,136 @@
'use strict';
const getDocsUrl = require('./utils/get-docs-url');
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved

const getArgumentNameForReplaceChildOrInsertBefore = nodeArguments => {
if (nodeArguments.type === 'Identifier') {
return nodeArguments.name;
}

return null;
};

const forbiddenIdentifierNames = new Map([
['replaceChild', 'replaceWith'],
['insertBefore', 'before']
]);

const checkForReplaceChildOrInsertBefore = (context, node) => {
const identifierName = node.callee.property.name;

// Return early when specified methods don't exist in forbiddenIdentifierNames
if (!forbiddenIdentifierNames.has(identifierName)) {
return;
}

const nodeArguments = node.arguments;
const newChildNodeArg = getArgumentNameForReplaceChildOrInsertBefore(
nodeArguments[0]
);
const oldChildNodeArg = getArgumentNameForReplaceChildOrInsertBefore(
nodeArguments[1]
);

// Return early in case that one of the provided arguments is not a node
if (!newChildNodeArg || !oldChildNodeArg) {
return;
}

const parentNode = node.callee.object.name;
// This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets transformed
if (!parentNode) {
return;
}

const preferredSelector = forbiddenIdentifierNames.get(identifierName);

return context.report({
node,
message: `Prefer \`${oldChildNodeArg}.${preferredSelector}(${newChildNodeArg})\` over \`${parentNode}.${identifierName}(${newChildNodeArg}, ${oldChildNodeArg})\`.`,
fix: fixer =>
fixer.replaceText(
node,
`${oldChildNodeArg}.${preferredSelector}(${newChildNodeArg})`
)
});
};

// Handle both Identifier and Literal because the preferred selectors support nodes and DOMString
const getArgumentNameForInsertAdjacentMethods = nodeArguments => {
if (nodeArguments.type === 'Identifier') {
return nodeArguments.name;
}

if (nodeArguments.type === 'Literal') {
return nodeArguments.raw;
}

return null;
};

const positionReplacers = new Map([
['beforebegin', 'before'],
['afterbegin', 'prepend'],
['beforeend', 'append'],
['afterend', 'after']
]);

const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => {
const identifierName = node.callee.property.name;

if (
identifierName !== 'insertAdjacentText' &&
identifierName !== 'insertAdjacentElement'
) {
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const nodeArguments = node.arguments;
const positionArg = getArgumentNameForInsertAdjacentMethods(nodeArguments[0]);
const positionAsValue = nodeArguments[0].value;

// Return early when specified position value of 1st argument is not a recognised value
if (!positionReplacers.has(positionAsValue)) {
return;
}

const referenceNode = node.callee.object.name;
const preferredSelector = positionReplacers.get(positionAsValue);
const insertedTextArg = getArgumentNameForInsertAdjacentMethods(
nodeArguments[1]
);

return context.report({
node,
message: `Prefer \`${referenceNode}.${preferredSelector}(${insertedTextArg})\` over \`${referenceNode}.${identifierName}(${positionArg}, ${insertedTextArg})\`.`,
fix: fixer =>
fixer.replaceText(
node,
`${referenceNode}.${preferredSelector}(${insertedTextArg})`
)
});
};

const create = context => {
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.arguments.length === 2
Copy link
Contributor

@MrHen MrHen Sep 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can include these checks in the selector pattern for the rule. Not required for the PR but in case you hadn't seen it yet, here are the docs: https://eslint.org/docs/developer-guide/selectors

) {
checkForReplaceChildOrInsertBefore(context, node);
checkForInsertAdjacentTextOrInsertAdjacentElement(context, node);
}
}
};
};

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocsUrl(__filename)
},
fixable: 'code'
}
};