Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
prefer-modern-dom-apis
rule (#362)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
- Loading branch information
1 parent
44a67f1
commit 44d14b9
Showing
6 changed files
with
892 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Prefer modern DOM APIs | ||
|
||
Enforces the use of: | ||
|
||
- [childNode.replaceWith(newNode)](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith) over [parentNode.replaceChild(newNode, oldNode)](https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild) | ||
- [referenceNode.before(newNode)](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) over [parentNode.insertBefore(newNode, referenceNode)](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) | ||
- [referenceNode.before('text')](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) over [referenceNode.insertAdjacentText('beforebegin', 'text')](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText) | ||
- [referenceNode.before(newNode)](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/before) over [referenceNode.insertAdjacentElement('beforebegin', newNode)](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) | ||
|
||
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') | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
'use strict'; | ||
const getDocumentationUrl = require('./utils/get-documentation-url'); | ||
|
||
const getArgumentNameForReplaceChildOrInsertBefore = nodeArguments => { | ||
if (nodeArguments.type === 'Identifier') { | ||
return nodeArguments.name; | ||
} | ||
}; | ||
|
||
const forbiddenIdentifierNames = new Map([ | ||
['replaceChild', 'replaceWith'], | ||
['insertBefore', 'before'] | ||
]); | ||
|
||
const isPartOfVariableAssignment = nodeParentType => { | ||
if (nodeParentType === 'VariableDeclarator' || nodeParentType === 'AssignmentExpression') { | ||
return true; | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
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 newChildNodeArgument = getArgumentNameForReplaceChildOrInsertBefore( | ||
nodeArguments[0] | ||
); | ||
const oldChildNodeArgument = getArgumentNameForReplaceChildOrInsertBefore( | ||
nodeArguments[1] | ||
); | ||
|
||
// Return early in case that one of the provided arguments is not a node | ||
if (!newChildNodeArgument || !oldChildNodeArgument) { | ||
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); | ||
|
||
let fix = fixer => fixer.replaceText( | ||
node, | ||
`${oldChildNodeArgument}.${preferredSelector}(${newChildNodeArgument})` | ||
); | ||
|
||
// Report error when the method is part of a variable assignment | ||
// but don't offer to autofix `.replaceWith()` and `.before()` | ||
// which don't have a return value. | ||
if (isPartOfVariableAssignment(node.parent.type)) { | ||
fix = undefined; | ||
} | ||
|
||
return context.report({ | ||
node, | ||
message: `Prefer \`${oldChildNodeArgument}.${preferredSelector}(${newChildNodeArgument})\` over \`${parentNode}.${identifierName}(${newChildNodeArgument}, ${oldChildNodeArgument})\`.`, | ||
fix | ||
}); | ||
}; | ||
|
||
// 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; | ||
} | ||
}; | ||
|
||
const positionReplacers = new Map([ | ||
['beforebegin', 'before'], | ||
['afterbegin', 'prepend'], | ||
['beforeend', 'append'], | ||
['afterend', 'after'] | ||
]); | ||
|
||
const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => { | ||
const identifierName = node.callee.property.name; | ||
|
||
// Return early when method name is not one of the targeted ones. | ||
if ( | ||
identifierName !== 'insertAdjacentText' && | ||
identifierName !== 'insertAdjacentElement' | ||
) { | ||
return; | ||
} | ||
|
||
const nodeArguments = node.arguments; | ||
const positionArgument = getArgumentNameForInsertAdjacentMethods(nodeArguments[0]); | ||
const positionAsValue = nodeArguments[0].value; | ||
|
||
// Return early when specified position value of first argument is not a recognized value. | ||
if (!positionReplacers.has(positionAsValue)) { | ||
return; | ||
} | ||
|
||
const referenceNode = node.callee.object.name; | ||
const preferredSelector = positionReplacers.get(positionAsValue); | ||
const insertedTextArgument = getArgumentNameForInsertAdjacentMethods( | ||
nodeArguments[1] | ||
); | ||
|
||
let fix = fixer => | ||
fixer.replaceText( | ||
node, | ||
`${referenceNode}.${preferredSelector}(${insertedTextArgument})` | ||
); | ||
|
||
// Report error when the method is part of a variable assignment | ||
// but don't offer to autofix `.insertAdjacentElement()` | ||
// which don't have a return value. | ||
if (identifierName === 'insertAdjacentElement' && isPartOfVariableAssignment(node.parent.type)) { | ||
fix = undefined; | ||
} | ||
|
||
return context.report({ | ||
node, | ||
message: `Prefer \`${referenceNode}.${preferredSelector}(${insertedTextArgument})\` over \`${referenceNode}.${identifierName}(${positionArgument}, ${insertedTextArgument})\`.`, | ||
fix | ||
}); | ||
}; | ||
|
||
const create = context => { | ||
return { | ||
CallExpression(node) { | ||
if ( | ||
node.callee.type === 'MemberExpression' && | ||
node.arguments.length === 2 | ||
) { | ||
checkForReplaceChildOrInsertBefore(context, node); | ||
checkForInsertAdjacentTextOrInsertAdjacentElement(context, node); | ||
} | ||
} | ||
}; | ||
}; | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: getDocumentationUrl(__filename) | ||
}, | ||
fixable: 'code' | ||
} | ||
}; |
Oops, something went wrong.