Skip to content

Commit

Permalink
[Refactor] use es-iterator-helpers
Browse files Browse the repository at this point in the history
 - also, maximally avoid iterator spreads
  • Loading branch information
ljharb committed Aug 13, 2023
1 parent 9a8edde commit 52de824
Show file tree
Hide file tree
Showing 22 changed files with 94 additions and 90 deletions.
4 changes: 2 additions & 2 deletions __tests__/src/rules/aria-unsupported-elements-test.js
Expand Up @@ -69,7 +69,7 @@ const invalidAriaValidityTests = domElements
}));

ruleTester.run('aria-unsupported-elements', rule, {
valid: parsers.all([].concat(roleValidityTests.concat(ariaValidityTests))).map(parserOptionsMapper),
invalid: parsers.all([].concat(invalidRoleValidityTests.concat(invalidAriaValidityTests)))
valid: parsers.all([].concat(roleValidityTests, ariaValidityTests)).map(parserOptionsMapper),
invalid: parsers.all([].concat(invalidRoleValidityTests, invalidAriaValidityTests))
.map(parserOptionsMapper),
});
8 changes: 6 additions & 2 deletions __tests__/src/rules/role-has-required-aria-props-test.js
Expand Up @@ -10,6 +10,10 @@

import { roles } from 'aria-query';
import { RuleTester } from 'eslint';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';

import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/role-has-required-aria-props';
Expand Down Expand Up @@ -38,7 +42,7 @@ const componentsSettings = {
};

// Create basic test cases using all valid role types.
const basicValidityTests = [...roles.keys()].map((role) => {
const basicValidityTests = toArray(map(iterFrom(roles.keys()), (role) => {
const {
requiredProps: requiredPropKeyValues,
} = roles.get(role);
Expand All @@ -48,7 +52,7 @@ const basicValidityTests = [...roles.keys()].map((role) => {
return {
code: `<div role="${role.toLowerCase()}" ${propChain} />`,
};
});
}));

ruleTester.run('role-has-required-aria-props', rule, {
valid: parsers.all([].concat(
Expand Down
38 changes: 20 additions & 18 deletions __tests__/src/rules/role-supports-aria-props-test.js
Expand Up @@ -14,6 +14,11 @@ import {
import { RuleTester } from 'eslint';
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';

import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/role-supports-aria-props';
Expand All @@ -26,8 +31,7 @@ const ruleTester = new RuleTester();

const generateErrorMessage = (attr, role, tag, isImplicit) => {
if (isImplicit) {
return `The attribute ${attr} is not supported by the role ${role}. \
This role is implicit on the element ${tag}.`;
return `The attribute ${attr} is not supported by the role ${role}. This role is implicit on the element ${tag}.`;
}

return `The attribute ${attr} is not supported by the role ${role}.`;
Expand All @@ -46,30 +50,28 @@ const componentsSettings = {
},
};

const nonAbstractRoles = [...roles.keys()].filter((role) => roles.get(role).abstract === false);
const nonAbstractRoles = toArray(filter(iterFrom(roles.keys()), (role) => roles.get(role).abstract === false));

const createTests = (rolesNames) => rolesNames.reduce((tests, role) => {
const {
props: propKeyValues,
} = roles.get(role);
const validPropsForRole = Object.keys(propKeyValues);
const invalidPropsForRole = [...aria.keys()]
.map((attribute) => attribute.toLowerCase())
.filter((attribute) => validPropsForRole.indexOf(attribute) === -1);
const invalidPropsForRole = filter(
map(iterFrom(aria.keys()), (attribute) => attribute.toLowerCase()),
(attribute) => validPropsForRole.indexOf(attribute) === -1,
);
const normalRole = role.toLowerCase();

const allTests = [];

allTests[0] = tests[0].concat(validPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
})));

allTests[1] = tests[1].concat(invalidPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})));

return allTests;
return [
tests[0].concat(validPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
}))),
tests[1].concat(toArray(map(invalidPropsForRole, (prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})))),
];
}, [[], []]);

const [validTests, invalidTests] = createTests(nonAbstractRoles);
Expand Down
4 changes: 1 addition & 3 deletions __tests__/src/util/isDOMElement-test.js
Expand Up @@ -4,11 +4,9 @@ import { elementType } from 'jsx-ast-utils';
import isDOMElement from '../../../src/util/isDOMElement';
import JSXElementMock from '../../../__mocks__/JSXElementMock';

const domElements = [...dom.keys()];

describe('isDOMElement', () => {
describe('DOM elements', () => {
domElements.forEach((el) => {
dom.forEach((_, el) => {
it(`should identify ${el} as a DOM element`, () => {
const element = JSXElementMock(el);
expect(isDOMElement(elementType(element.openingElement)))
Expand Down
2 changes: 1 addition & 1 deletion __tests__/src/util/isFocusable-test.js
Expand Up @@ -9,7 +9,7 @@ import {
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';

function mergeTabIndex(index, attributes) {
return [...attributes, JSXAttributeMock('tabIndex', index)];
return [].concat(attributes, JSXAttributeMock('tabIndex', index));
}

describe('isFocusable', () => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -83,6 +83,7 @@
"axobject-query": "^3.2.1",
"damerau-levenshtein": "^1.0.8",
"emoji-regex": "^9.2.2",
"es-iterator-helpers": "^1.0.15",
"hasown": "^2.0.0",
"jsx-ast-utils": "^3.3.5",
"language-tags": "^1.0.9",
Expand Down
4 changes: 1 addition & 3 deletions src/rules/aria-activedescendant-has-tabindex.js
Expand Up @@ -18,8 +18,6 @@ const errorMessage = 'An element that manages focus with `aria-activedescendant`

const schema = generateObjSchema();

const domElements = [...dom.keys()];

export default {
meta: {
docs: {
Expand All @@ -42,7 +40,7 @@ export default {
const type = elementType(node);
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
if (domElements.indexOf(type) === -1) {
if (!dom.has(type)) {
return;
}
const tabIndex = getTabIndex(getProp(attributes, 'tabIndex'));
Expand Down
2 changes: 1 addition & 1 deletion src/rules/aria-props.js
Expand Up @@ -45,7 +45,7 @@ export default {
return;
}

const isValid = ariaAttributes.indexOf(name) > -1;
const isValid = aria.has(name);

if (isValid === false) {
context.report({
Expand Down
5 changes: 4 additions & 1 deletion src/rules/aria-role.js
Expand Up @@ -9,6 +9,9 @@

import { dom, roles } from 'aria-query';
import { getLiteralPropValue, propName } from 'jsx-ast-utils';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';

import getElementType from '../util/getElementType';
import { generateObjSchema } from '../util/schemas';

Expand All @@ -28,7 +31,7 @@ const schema = generateObjSchema({
},
});

const validRoles = new Set([...roles.keys()].filter((role) => roles.get(role).abstract === false));
const validRoles = new Set(filter(iterFrom(roles.keys()), (role) => roles.get(role).abstract === false));

export default {
meta: {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/aria-unsupported-elements.js
Expand Up @@ -47,7 +47,7 @@ export default {
return;
}

const invalidAttributes = [...aria.keys(), 'role'];
const invalidAttributes = new Set([...aria.keys(), 'role']);

node.attributes.forEach((prop) => {
if (prop.type === 'JSXSpreadAttribute') {
Expand All @@ -56,7 +56,7 @@ export default {

const name = propName(prop).toLowerCase();

if (invalidAttributes.indexOf(name) > -1) {
if (invalidAttributes.has(name)) {
context.report({
node,
message: errorMessage(name),
Expand Down
4 changes: 1 addition & 3 deletions src/rules/click-events-have-key-events.js
Expand Up @@ -9,7 +9,6 @@

import { dom } from 'aria-query';
import { getProp, hasAnyProp } from 'jsx-ast-utils';
import includes from 'array-includes';
import { generateObjSchema } from '../util/schemas';
import getElementType from '../util/getElementType';
import isHiddenFromScreenReader from '../util/isHiddenFromScreenReader';
Expand All @@ -19,7 +18,6 @@ import isPresentationRole from '../util/isPresentationRole';
const errorMessage = 'Visible, non-interactive elements with click handlers must have at least one keyboard listener.';

const schema = generateObjSchema();
const domElements = [...dom.keys()];

export default {
meta: {
Expand All @@ -42,7 +40,7 @@ export default {
const type = elementType(node);
const requiredProps = ['onkeydown', 'onkeyup', 'onkeypress'];

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
4 changes: 2 additions & 2 deletions src/rules/interactive-supports-focus.js
Expand Up @@ -36,12 +36,12 @@ import getTabIndex from '../util/getTabIndex';
// ----------------------------------------------------------------------------

const schema = generateObjSchema({
// TODO: convert to use iterFilter and iterFrom
tabbable: enumArraySchema([...roles.keys()].filter((name) => (
!roles.get(name).abstract
&& roles.get(name).superClass.some((klasses) => includes(klasses, 'widget'))
))),
});
const domElements = [...dom.keys()];

const interactiveProps = [].concat(
eventHandlersByType.mouse,
Expand Down Expand Up @@ -69,7 +69,7 @@ export default ({
const hasInteractiveProps = hasAnyProp(attributes, interactiveProps);
const hasTabindex = getTabIndex(getProp(attributes, 'tabIndex')) !== undefined;

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
4 changes: 1 addition & 3 deletions src/rules/no-interactive-element-to-noninteractive-role.js
Expand Up @@ -27,8 +27,6 @@ import isPresentationRole from '../util/isPresentationRole';

const errorMessage = 'Interactive elements should not be assigned non-interactive roles.';

const domElements = [...dom.keys()];

export default ({
meta: {
docs: {
Expand Down Expand Up @@ -62,7 +60,7 @@ export default ({
const type = elementType(node);
const role = getLiteralPropValue(getProp(node.attributes, 'role'));

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
3 changes: 1 addition & 2 deletions src/rules/no-noninteractive-element-interactions.js
Expand Up @@ -32,7 +32,6 @@ import isPresentationRole from '../util/isPresentationRole';

const errorMessage = 'Non-interactive elements should not be assigned mouse or keyboard event listeners.';

const domElements = [...dom.keys()];
const defaultInteractiveProps = [].concat(
eventHandlersByType.focus,
eventHandlersByType.image,
Expand Down Expand Up @@ -72,7 +71,7 @@ export default ({
&& getPropValue(getProp(attributes, prop)) != null
));

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
4 changes: 1 addition & 3 deletions src/rules/no-noninteractive-element-to-interactive-role.js
Expand Up @@ -25,8 +25,6 @@ import isInteractiveRole from '../util/isInteractiveRole';

const errorMessage = 'Non-interactive elements should not be assigned interactive roles.';

const domElements = [...dom.keys()];

export default ({
meta: {
docs: {
Expand Down Expand Up @@ -60,7 +58,7 @@ export default ({
const type = elementType(node);
const role = getExplicitRole(type, node.attributes);

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
4 changes: 1 addition & 3 deletions src/rules/no-static-element-interactions.js
Expand Up @@ -16,7 +16,6 @@ import {
hasProp,
} from 'jsx-ast-utils';
import type { JSXOpeningElement } from 'ast-types-flow';
import includes from 'array-includes';
import type { ESLintConfig, ESLintContext, ESLintVisitorSelectorConfig } from '../../flow/eslint';
import { arraySchema, generateObjSchema } from '../util/schemas';
import getElementType from '../util/getElementType';
Expand All @@ -31,7 +30,6 @@ import isPresentationRole from '../util/isPresentationRole';

const errorMessage = 'Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.';

const domElements = [...dom.keys()];
const defaultInteractiveProps = [].concat(
eventHandlersByType.focus,
eventHandlersByType.keyboard,
Expand Down Expand Up @@ -69,7 +67,7 @@ export default ({
&& getPropValue(getProp(attributes, prop)) != null
));

if (!includes(domElements, type)) {
if (!dom.has(type)) {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
return;
Expand Down
11 changes: 6 additions & 5 deletions src/rules/role-supports-aria-props.js
Expand Up @@ -18,14 +18,16 @@ import {
getPropValue,
propName,
} from 'jsx-ast-utils';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';

import { generateObjSchema } from '../util/schemas';
import getElementType from '../util/getElementType';
import getImplicitRole from '../util/getImplicitRole';

const errorMessage = (attr, role, tag, isImplicit) => {
if (isImplicit) {
return `The attribute ${attr} is not supported by the role ${role}. \
This role is implicit on the element ${tag}.`;
return `The attribute ${attr} is not supported by the role ${role}. This role is implicit on the element ${tag}.`;
}

return `The attribute ${attr} is not supported by the role ${role}.`;
Expand Down Expand Up @@ -66,15 +68,14 @@ export default {
const {
props: propKeyValues,
} = roles.get(roleValue);
const invalidAriaPropsForRole = [...aria.keys()]
.filter((attribute) => !(attribute in propKeyValues));
const invalidAriaPropsForRole = new Set(filter(iterFrom(aria.keys()), (attribute) => !(attribute in propKeyValues)));

node.attributes.filter((prop) => (
getPropValue(prop) != null // Ignore the attribute if its value is null or undefined.
&& prop.type !== 'JSXSpreadAttribute' // Ignore the attribute if it's a spread.
)).forEach((prop) => {
const name = propName(prop);
if (invalidAriaPropsForRole.indexOf(name) > -1) {
if (invalidAriaPropsForRole.has(name)) {
context.report({
node,
message: errorMessage(name, roleValue, type, isImplicit),
Expand Down
9 changes: 5 additions & 4 deletions src/util/isAbstractRole.js
Expand Up @@ -3,16 +3,17 @@ import {
roles,
} from 'aria-query';
import { getProp, getLiteralPropValue } from 'jsx-ast-utils';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';

const abstractRoles = new Set([...roles.keys()]
.filter((role) => roles.get(role).abstract));
const abstractRoles = new Set(filter(iterFrom(roles.keys()), (role) => roles.get(role).abstract));

const DOMElements = [...dom.keys()];
const DOMElements = new Set(dom.keys());

const isAbstractRole = (tagName, attributes) => {
// Do not test higher level JSX components, as we do not know what
// low-level DOM element this maps to.
if (DOMElements.indexOf(tagName) === -1) {
if (!DOMElements.has(tagName)) {
return false;
}

Expand Down

0 comments on commit 52de824

Please sign in to comment.