From 44d14b9c3df20be910e52aecec44ea49d1b08d38 Mon Sep 17 00:00:00 2001 From: Markus Hatvan Date: Sun, 22 Dec 2019 18:27:23 +0100 Subject: [PATCH] Add `prefer-modern-dom-apis` rule (#362) Co-authored-by: Sindre Sorhus --- docs/rules/prefer-modern-dom-apis.md | 52 +++ index.js | 1 + readme.md | 4 +- rules/prefer-modern-dom-apis.js | 158 +++++++ .../package-lock.json | 400 +++++++++++++++++- test/prefer-modern-dom-apis.js | 280 ++++++++++++ 6 files changed, 892 insertions(+), 3 deletions(-) create mode 100644 docs/rules/prefer-modern-dom-apis.md create mode 100644 rules/prefer-modern-dom-apis.js create mode 100644 test/prefer-modern-dom-apis.js diff --git a/docs/rules/prefer-modern-dom-apis.md b/docs/rules/prefer-modern-dom-apis.md new file mode 100644 index 0000000000..e9dd1c0956 --- /dev/null +++ b/docs/rules/prefer-modern-dom-apis.md @@ -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') +``` diff --git a/index.js b/index.js index 6cbb786f59..fec6e2a4b8 100644 --- a/index.js +++ b/index.js @@ -49,6 +49,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-negative-index': 'error', 'unicorn/prefer-node-append': 'error', 'unicorn/prefer-node-remove': 'error', diff --git a/readme.md b/readme.md index 534eb98321..923fa503cd 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ You might want to check out [XO](https://github.com/xojs/xo), which includes thi ## Install -``` +```console $ npm install --save-dev eslint eslint-plugin-unicorn ``` @@ -67,6 +67,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-negative-index": "error", "unicorn/prefer-node-append": "error", "unicorn/prefer-node-remove": "error", @@ -120,6 +121,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-negative-index](docs/rules/prefer-negative-index.md) - Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()` and `Array#splice()`. *(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)* diff --git a/rules/prefer-modern-dom-apis.js b/rules/prefer-modern-dom-apis.js new file mode 100644 index 0000000000..d8f4214881 --- /dev/null +++ b/rules/prefer-modern-dom-apis.js @@ -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' + } +}; diff --git a/test/integration/eslint-config-unicorn-tester/package-lock.json b/test/integration/eslint-config-unicorn-tester/package-lock.json index d2ca59e3c7..55060d39ca 100644 --- a/test/integration/eslint-config-unicorn-tester/package-lock.json +++ b/test/integration/eslint-config-unicorn-tester/package-lock.json @@ -393,14 +393,410 @@ "lodash.defaultsdeep": "^4.6.1", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", - "lodash.topairs": "^4.3.0", "lodash.upperfirst": "^4.3.1", "read-pkg-up": "^7.0.0", - "regexp-tree": "^0.1.16", + "regexp-tree": "^0.1.17", "regexpp": "^3.0.0", "reserved-words": "^0.1.2", "safe-regex": "^2.1.1", "semver": "^6.3.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" + }, + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==" + }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-ast-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz", + "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==", + "requires": { + "lodash.get": "^4.4.2", + "lodash.zip": "^4.2.0" + } + }, + "eslint-template-visitor": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-template-visitor/-/eslint-template-visitor-1.1.0.tgz", + "integrity": "sha512-Lmy6QVlmFiIGl5fPi+8ACnov3sare+0Ouf7deJAGGhmUfeWJ5fVarELUxZRpsZ9sHejiJUq8626d0dn9uvcZTw==", + "requires": { + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.1", + "multimap": "^1.0.2" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hosted-git-info": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==" + }, + "import-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.0.0.tgz", + "integrity": "sha512-iczM/v9drffdNnABOKwj0f9G3cFDon99VcG1mxeBsdqnbd+vnQ5c2uAiCHNQITqFTOPaEvwg3VjoWCur0uHLEw==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=" + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=" + }, + "lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=" + }, + "multimap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz", + "integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "regexp-tree": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.17.tgz", + "integrity": "sha512-UnOJjFS/EPZmfISmYx+0PcDtPzyFKTe+cZTS5sM5hifnRUDRxoB1j4DAmGwqzxjwBGlwOkGfb2cDGHtjuEwqoA==" + }, + "regexpp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==" + }, + "reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=" + }, + "resolve": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", + "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "requires": { + "regexp-tree": "~0.1.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + } } }, "eslint-scope": { diff --git a/test/prefer-modern-dom-apis.js b/test/prefer-modern-dom-apis.js new file mode 100644 index 0000000000..0ee6595cea --- /dev/null +++ b/test/prefer-modern-dom-apis.js @@ -0,0 +1,280 @@ +import test from 'ava'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import {outdent} from 'outdent'; +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);' + }, + { + code: outdent` + parentNode.replaceChild( + newChildNode, + oldChildNode + ); + `, + errors: [ + { + message: + 'Prefer `oldChildNode.replaceWith(newChildNode)` over `parentNode.replaceChild(newChildNode, oldChildNode)`.' + } + ], + output: 'oldChildNode.replaceWith(newChildNode);' + }, + { + code: outdent` + parentNode.replaceChild( // inline comments + newChildNode, // inline comments + oldChildNode // inline comments + ); + `, + errors: [ + { + message: + 'Prefer `oldChildNode.replaceWith(newChildNode)` over `parentNode.replaceChild(newChildNode, oldChildNode)`.' + } + ], + output: 'oldChildNode.replaceWith(newChildNode);' + }, + { + code: 'const foo = parentNode.replaceChild(newChildNode, oldChildNode);', + errors: [ + { + message: + 'Prefer `oldChildNode.replaceWith(newChildNode)` over `parentNode.replaceChild(newChildNode, oldChildNode)`.' + } + ], + output: 'const foo = parentNode.replaceChild(newChildNode, oldChildNode);' + }, + { + code: 'foo = parentNode.replaceChild(newChildNode, oldChildNode);', + errors: [ + { + message: + 'Prefer `oldChildNode.replaceWith(newChildNode)` over `parentNode.replaceChild(newChildNode, oldChildNode)`.' + } + ], + output: 'foo = parentNode.replaceChild(newChildNode, oldChildNode);' + }, + // Tests for .insertBefore() + { + code: 'parentNode.insertBefore(newNode, referenceNode);', + errors: [ + { + message: + 'Prefer `referenceNode.before(newNode)` over `parentNode.insertBefore(newNode, referenceNode)`.' + } + ], + output: 'referenceNode.before(newNode);' + }, + { + code: 'parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta);', + errors: [ + { + message: + 'Prefer `beta.before(alfa)` over `parentNode.insertBefore(alfa, beta)`.' + } + ], + output: 'beta.before(alfa).insertBefore(charlie, delta);' + }, + { + code: 'const foo = parentNode.insertBefore(alfa, beta);', + errors: [ + { + message: + 'Prefer `beta.before(alfa)` over `parentNode.insertBefore(alfa, beta)`.' + } + ], + output: 'const foo = parentNode.insertBefore(alfa, beta);' + }, + { + code: 'foo = parentNode.insertBefore(alfa, beta);', + errors: [ + { + message: + 'Prefer `beta.before(alfa)` over `parentNode.insertBefore(alfa, beta)`.' + } + ], + output: 'foo = parentNode.insertBefore(alfa, beta);' + }, + // 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");' + }, + { + code: 'const foo = referenceNode.insertAdjacentText("beforebegin", "text");', + errors: [ + { + message: + 'Prefer `referenceNode.before("text")` over `referenceNode.insertAdjacentText("beforebegin", "text")`.' + } + ], + output: 'const foo = referenceNode.before("text");' + }, + { + code: 'foo = referenceNode.insertAdjacentText("beforebegin", "text");', + errors: [ + { + message: + 'Prefer `referenceNode.before("text")` over `referenceNode.insertAdjacentText("beforebegin", "text")`.' + } + ], + output: 'foo = referenceNode.before("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);' + }, + { + code: outdent` + referenceNode.insertAdjacentElement( + "afterend", + newNode + ); + `, + errors: [ + { + message: + 'Prefer `referenceNode.after(newNode)` over `referenceNode.insertAdjacentElement("afterend", newNode)`.' + } + ], + output: 'referenceNode.after(newNode);' + }, + { + code: outdent` + referenceNode.insertAdjacentElement( // inline comments + "afterend", // inline comments + newNode // inline comments + ); // inline comments + `, + errors: [ + { + message: + 'Prefer `referenceNode.after(newNode)` over `referenceNode.insertAdjacentElement("afterend", newNode)`.' + } + ], + output: 'referenceNode.after(newNode); // inline comments' + }, + { + code: 'const foo = referenceNode.insertAdjacentElement("beforebegin", newNode);', + errors: [ + { + message: + 'Prefer `referenceNode.before(newNode)` over `referenceNode.insertAdjacentElement("beforebegin", newNode)`.' + } + ], + output: 'const foo = referenceNode.insertAdjacentElement("beforebegin", newNode);' + }, + { + code: 'foo = referenceNode.insertAdjacentElement("beforebegin", newNode);', + errors: [ + { + message: + 'Prefer `referenceNode.before(newNode)` over `referenceNode.insertAdjacentElement("beforebegin", newNode)`.' + } + ], + output: 'foo = referenceNode.insertAdjacentElement("beforebegin", newNode);' + } + ] +});