From 922d10a66243f9a595d7c656618aecedc4ba8de5 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 5 Oct 2023 17:00:09 +0200 Subject: [PATCH] Modify rule S6747 (`no-unknown-property `): add `jsx-a11y/aria-props` --- .../jsts/vuetify/typescript-S6747.json | 3 + package-lock.json | 180 ++++++++++++++++++ package.json | 2 + packages/jsts/src/rules/S6747/cb.fixture.jsx | 2 +- packages/jsts/src/rules/S6747/index.ts | 5 +- packages/jsts/src/rules/S6747/rule.ts | 83 ++++++++ .../declarations/eslint-plugin-jsx-a11y.ts | 1 + 7 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 packages/jsts/src/rules/S6747/rule.ts create mode 100644 packages/jsts/src/rules/helpers/declarations/eslint-plugin-jsx-a11y.ts diff --git a/its/ruling/src/test/expected/jsts/vuetify/typescript-S6747.json b/its/ruling/src/test/expected/jsts/vuetify/typescript-S6747.json index 636d7c7ef55..d91141067f1 100644 --- a/its/ruling/src/test/expected/jsts/vuetify/typescript-S6747.json +++ b/its/ruling/src/test/expected/jsts/vuetify/typescript-S6747.json @@ -115,6 +115,9 @@ 40, 41 ], +"vuetify:packages/vuetify/src/components/VDialog/VDialog.tsx": [ +120 +], "vuetify:packages/vuetify/src/components/VDivider/VDivider.tsx": [ 47 ], diff --git a/package-lock.json b/package-lock.json index 48d17f580b0..16e2bc4b6a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-sonarjs", + "eslint-plugin-jsx-a11y", "express", "functional-red-black-tree", "htmlparser2", @@ -57,6 +58,7 @@ "builtin-modules": "3.3.0", "bytes": "3.1.2", "eslint": "8.47.0", + "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-sonarjs": "0.21.0", @@ -3625,6 +3627,15 @@ "inBundle": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "inBundle": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "inBundle": true, @@ -3750,6 +3761,12 @@ "node": ">=8" } }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", + "inBundle": true + }, "node_modules/astral-regex": { "version": "2.0.0", "inBundle": true, @@ -3777,6 +3794,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.8.2", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/axe-core/-/axe-core-4.8.2.tgz", + "integrity": "sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "inBundle": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "dev": true, @@ -4832,6 +4867,12 @@ "node": ">=4" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "inBundle": true + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "dev": true, @@ -5039,6 +5080,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "inBundle": true, @@ -5390,6 +5440,42 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "inBundle": true, + "dependencies": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "inBundle": true + }, "node_modules/eslint-plugin-react": { "version": "7.33.2", "inBundle": true, @@ -8792,6 +8878,21 @@ "inBundle": true, "license": "MIT" }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "inBundle": true + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "inBundle": true, + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, "node_modules/leven": { "version": "3.1.0", "dev": true, @@ -14362,6 +14463,14 @@ "argparse": { "version": "2.0.1" }, + "aria-query": { + "version": "5.3.0", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "requires": { + "dequal": "^2.0.3" + } + }, "array-buffer-byte-length": { "version": "1.0.0", "requires": { @@ -14433,6 +14542,11 @@ "version": "2.0.1", "dev": true }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, "astral-regex": { "version": "2.0.0" }, @@ -14445,6 +14559,19 @@ "available-typed-arrays": { "version": "1.0.5" }, + "axe-core": { + "version": "4.8.2", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/axe-core/-/axe-core-4.8.2.tgz", + "integrity": "sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==" + }, + "axobject-query": { + "version": "3.2.1", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "requires": { + "dequal": "^2.0.3" + } + }, "babel-jest": { "version": "29.7.0", "dev": true, @@ -15101,6 +15228,11 @@ "cssesc": { "version": "3.0.0" }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, "data-uri-to-buffer": { "version": "4.0.1", "dev": true @@ -15222,6 +15354,11 @@ "depd": { "version": "2.0.0" }, + "dequal": { + "version": "2.0.3", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + }, "destroy": { "version": "1.2.0" }, @@ -15497,6 +15634,36 @@ } } }, + "eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "requires": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "dependencies": { + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + } + } + }, "eslint-plugin-react": { "version": "7.33.2", "requires": { @@ -17423,6 +17590,19 @@ "known-css-properties": { "version": "0.27.0" }, + "language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, "leven": { "version": "3.1.0", "dev": true diff --git a/package.json b/package.json index dc2947831af..210ed055f81 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "builtin-modules": "3.3.0", "bytes": "3.1.2", "eslint": "8.47.0", + "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-sonarjs": "0.21.0", @@ -119,6 +120,7 @@ "eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-sonarjs", + "eslint-plugin-jsx-a11y", "express", "functional-red-black-tree", "htmlparser2", diff --git a/packages/jsts/src/rules/S6747/cb.fixture.jsx b/packages/jsts/src/rules/S6747/cb.fixture.jsx index 3da0b83ccae..fd6b157847a 100644 --- a/packages/jsts/src/rules/S6747/cb.fixture.jsx +++ b/packages/jsts/src/rules/S6747/cb.fixture.jsx @@ -2,5 +2,5 @@ // ^^^^^^^^^^^ // fix@qf1 {{Replace with 'className'}} // edit@qf1 [[sc=0;ec=24]] {{
;}} -
; // Noncompliant +
; // Noncompliant {{aria-foo: This attribute is an invalid ARIA attribute.}} diff --git a/packages/jsts/src/rules/S6747/index.ts b/packages/jsts/src/rules/S6747/index.ts index e187ed8a3d0..13438fe7e69 100644 --- a/packages/jsts/src/rules/S6747/index.ts +++ b/packages/jsts/src/rules/S6747/index.ts @@ -17,7 +17,4 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { rules } from 'eslint-plugin-react'; -import { decorate } from './decorator'; - -export const rule = decorate(rules['no-unknown-property']); +export { rule } from './rule'; diff --git a/packages/jsts/src/rules/S6747/rule.ts b/packages/jsts/src/rules/S6747/rule.ts new file mode 100644 index 00000000000..15cbf57ef71 --- /dev/null +++ b/packages/jsts/src/rules/S6747/rule.ts @@ -0,0 +1,83 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// https://sonarsource.github.io/rspec/#/rspec/S1534/javascript + +import { Rule } from 'eslint'; +import { rules as reactRules } from 'eslint-plugin-react'; +import { rules as jsxA11yRules } from 'eslint-plugin-jsx-a11y'; +import { interceptReport, mergeRules } from '../helpers'; +import { decorate } from './decorator'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +const noUnkownProp = reactRules['no-unknown-property']; +const decoratedNoUnkownProp = decorate(noUnkownProp); + +/** + * We keep a single occurence of issues raised by both rules, discarding the ones raised by 'no-async-promise-executor' + * The current logic relies on the fact that the listener of 'no-misused-promises' runs first because + * it is alphabetically "smaller", which is how we set them up in mergeRules. + */ + +/** + * start offsets of nodes that raised issues in typescript-eslint's no-misused-promises + */ +const flaggedNodeStarts = new Map(); + +const ariaPropsRule = jsxA11yRules['aria-props']; +const decoratedAriaPropsRule = interceptReport(ariaPropsRule, (context, descriptor) => { + if ('node' in descriptor) { + const start = (descriptor.node as TSESTree.Node).range[0]; + if (!flaggedNodeStarts.get(start)) { + flaggedNodeStarts.set(start, true); + context.report(descriptor); + } + } +}); + +const twiceDecoratedNoUnkownProp = interceptReport(decoratedNoUnkownProp, (context, descriptor) => { + if ('node' in descriptor) { + const start = (descriptor.node as TSESTree.Node).range[0]; + if (!flaggedNodeStarts.get(start)) { + context.report(descriptor); + } + } +}); + +export const rule: Rule.RuleModule = { + /** + * The metadata from `no-dupe-class-members` and `jsx-no-duplicate-props` are required for issue messages. + * However, we don't include those from `no-dupe-keys` because of a duplicate message id, and we use instead + * the message id from `no-dupe-class-members`, which is a bit more generic. + */ + meta: { + hasSuggestions: true, + messages: { + ...decoratedAriaPropsRule.meta!.messages, + ...twiceDecoratedNoUnkownProp.meta!.messages, + }, + }, + + create(context: Rule.RuleContext) { + const ariaPropsListener: Rule.RuleListener = decoratedAriaPropsRule.create(context); + const noUnkownPropListener: Rule.RuleListener = twiceDecoratedNoUnkownProp.create(context); + + return mergeRules(ariaPropsListener, noUnkownPropListener); + }, +}; diff --git a/packages/jsts/src/rules/helpers/declarations/eslint-plugin-jsx-a11y.ts b/packages/jsts/src/rules/helpers/declarations/eslint-plugin-jsx-a11y.ts new file mode 100644 index 00000000000..3c60e483114 --- /dev/null +++ b/packages/jsts/src/rules/helpers/declarations/eslint-plugin-jsx-a11y.ts @@ -0,0 +1 @@ +declare module 'eslint-plugin-jsx-a11y';