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 1 commit
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
14 changes: 5 additions & 9 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,19 +140,17 @@ 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)
- [Sam Verschueren](https://github.com/SamVerschueren)
- [futpib](https://github.com/futpib)
- [Adam Babcock](https://github.com/MrHen)

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

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


## License
#### License

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

const checkForReplaceChildOrInsertBefore = (context, node) => {
const getArgumentName = args => {
if (args.type === 'Identifier') {
return args.name;
}

return null;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this helper function out of checkForReplaceChildOrInsertBefore. There may be a suitable util here: https://github.com/sindresorhus/eslint-plugin-unicorn/tree/master/rules/utils. Perhaps is-method-named?

Also, args is not a very good argument name. Perhaps nodeArgument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is-method-named would not help me, since i cannot say what the argument name will be.
There are multiple other rules with const args = node.arguments; and their own scoped helper functions, e.g: prefer-node-remove in the code already, but I am also fine with renaming it ;)


const forbiddenIdentifierNames = new Map([
['replaceChild', 'replaceWith'],
['insertBefore', 'before']
]);
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved

const identifierName = node.callee.property.name;

// Only handle methods that exist in forbiddenIdentifierNames
if (forbiddenIdentifierNames.has(identifierName)) {
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved
const args = node.arguments;
const newChildNodeArg = getArgumentName(args[0]);
const oldChildNodeArg = getArgumentName(args[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;
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})`
)
});
}
};

const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => {
// Handle both Identifier and Literal because the preferred selectors support nodes and DOMString
const getArgumentName = args => {
if (args.type === 'Identifier') {
return args.name;
}

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

return null;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as before. This should be moved to the parent scope.


const positionReplacers = new Map([
['beforebegin', 'before'],
['afterbegin', 'prepend'],
['beforeend', 'append'],
['afterend', 'after']
]);
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved

const identifierName = node.callee.property.name;

if (
identifierName === 'insertAdjacentText' ||
identifierName === 'insertAdjacentElement'
) {
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved
const args = node.arguments;
const positionArg = getArgumentName(args[0]);
const positionAsValue = args[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 = getArgumentName(args[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'
}
};
129 changes: 129 additions & 0 deletions test/prefer-modern-dom-apis.js
@@ -0,0 +1,129 @@
import test from 'ava';
import avaRuleTester from 'eslint-ava-rule-tester';
import rule from '../rules/prefer-modern-dom-apis';

const ruleTester = avaRuleTester(test, {
env: {
es6: true
}
});

ruleTester.run('prefer-modern-dom-apis', rule, {
valid: [
'oldChildNode.replaceWith(newChildNode);',
'referenceNode.before(newNode);',
'referenceNode.before("text");',
'referenceNode.prepend(newNode);',
'referenceNode.prepend("text");',
'referenceNode.append(newNode);',
'referenceNode.append("text");',
'referenceNode.after(newNode);',
'referenceNode.after("text");'
],
invalid: [
// Tests for .replaceChild()
{
code: 'parentNode.replaceChild(newChildNode, oldChildNode);',
errors: [
{
message:
'Prefer `oldChildNode.replaceWith(newChildNode)` over `parentNode.replaceChild(newChildNode, oldChildNode)`.'
}
],
output: 'oldChildNode.replaceWith(newChildNode);'
},
// Tests for .insertBefore()
{
code: 'parentNode.insertBefore(newNode, referenceNode);',
errors: [
{
message:
'Prefer `referenceNode.before(newNode)` over `parentNode.insertBefore(newNode, referenceNode)`.'
}
],
output: 'referenceNode.before(newNode);'
},
// Tests for .insertAdjacentText()
{
code: 'referenceNode.insertAdjacentText("beforebegin", "text");',
errors: [
{
message:
'Prefer `referenceNode.before("text")` over `referenceNode.insertAdjacentText("beforebegin", "text")`.'
}
],
output: 'referenceNode.before("text");'
},
{
code: 'referenceNode.insertAdjacentText("afterbegin", "text");',
errors: [
{
message:
'Prefer `referenceNode.prepend("text")` over `referenceNode.insertAdjacentText("afterbegin", "text")`.'
}
],
output: 'referenceNode.prepend("text");'
},
{
code: 'referenceNode.insertAdjacentText("beforeend", "text");',
errors: [
{
message:
'Prefer `referenceNode.append("text")` over `referenceNode.insertAdjacentText("beforeend", "text")`.'
}
],
output: 'referenceNode.append("text");'
},
{
code: 'referenceNode.insertAdjacentText("afterend", "text");',
errors: [
{
message:
'Prefer `referenceNode.after("text")` over `referenceNode.insertAdjacentText("afterend", "text")`.'
}
],
output: 'referenceNode.after("text");'
},
// Tests for .insertAdjacentElement()
{
code: 'referenceNode.insertAdjacentElement("beforebegin", newNode);',
errors: [
{
message:
'Prefer `referenceNode.before(newNode)` over `referenceNode.insertAdjacentElement("beforebegin", newNode)`.'
}
],
output: 'referenceNode.before(newNode);'
},
{
code: 'referenceNode.insertAdjacentElement("afterbegin", "text");',
errors: [
{
message:
'Prefer `referenceNode.prepend("text")` over `referenceNode.insertAdjacentElement("afterbegin", "text")`.'
}
],
output: 'referenceNode.prepend("text");'
},
{
code: 'referenceNode.insertAdjacentElement("beforeend", "text");',
errors: [
{
message:
'Prefer `referenceNode.append("text")` over `referenceNode.insertAdjacentElement("beforeend", "text")`.'
}
],
output: 'referenceNode.append("text");'
},
{
code: 'referenceNode.insertAdjacentElement("afterend", newNode);',
errors: [
{
message:
'Prefer `referenceNode.after(newNode)` over `referenceNode.insertAdjacentElement("afterend", newNode)`.'
}
],
output: 'referenceNode.after(newNode);'
}
]
});
mcmxcdev marked this conversation as resolved.
Show resolved Hide resolved