@hypersolid-dev/eslint-config 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +28 -1
- package/dist/base.d.ts +1 -0
- package/dist/base.js +63 -8
- package/dist/plugins/react-native-a11y/constants/accessibility-props.d.ts +32 -0
- package/dist/plugins/react-native-a11y/constants/accessibility-props.js +50 -0
- package/dist/plugins/react-native-a11y/constants/accessibility-state.d.ts +2 -0
- package/dist/plugins/react-native-a11y/constants/accessibility-state.js +8 -0
- package/dist/plugins/react-native-a11y/constants/interactive.d.ts +6 -0
- package/dist/plugins/react-native-a11y/constants/interactive.js +33 -0
- package/dist/plugins/react-native-a11y/constants/react-native-components.d.ts +7 -0
- package/dist/plugins/react-native-a11y/constants/react-native-components.js +13 -0
- package/dist/plugins/react-native-a11y/constants/schema-properties.d.ts +4 -0
- package/dist/plugins/react-native-a11y/constants/schema-properties.js +10 -0
- package/dist/plugins/react-native-a11y/index.d.ts +3 -0
- package/dist/plugins/react-native-a11y/index.js +48 -0
- package/dist/plugins/react-native-a11y/models/optional-options.d.ts +3 -0
- package/dist/plugins/react-native-a11y/models/optional-options.js +1 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +82 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +74 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-label.d.ts +6 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-label.js +103 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-role.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-role.js +72 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +49 -0
- package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +40 -0
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +77 -0
- package/dist/plugins/react-native-a11y/rules/input-purpose.d.ts +6 -0
- package/dist/plugins/react-native-a11y/rules/input-purpose.js +84 -0
- package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +57 -0
- package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.d.ts +4 -0
- package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +31 -0
- package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +66 -0
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +49 -0
- package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.d.ts +9 -0
- package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +76 -0
- package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +82 -0
- package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +70 -0
- package/dist/plugins/react-native-a11y/rules/no-press-in-activation.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +59 -0
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +75 -0
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +62 -0
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.d.ts +5 -0
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +55 -0
- package/dist/plugins/react-native-a11y/utils/create-rule.d.ts +5 -0
- package/dist/plugins/react-native-a11y/utils/create-rule.js +3 -0
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.d.ts +8 -0
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +67 -0
- package/dist/plugins/react-native-a11y/utils/get-components.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/get-components.js +11 -0
- package/dist/plugins/react-native-a11y/utils/get-element-name.d.ts +10 -0
- package/dist/plugins/react-native-a11y/utils/get-element-name.js +66 -0
- package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.d.ts +4 -0
- package/dist/plugins/react-native-a11y/utils/get-jsx-descendants.js +49 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-property-value.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-property-value.js +21 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-text-child.d.ts +8 -0
- package/dist/plugins/react-native-a11y/utils/get-literal-text-child.js +40 -0
- package/dist/plugins/react-native-a11y/utils/get-property.d.ts +4 -0
- package/dist/plugins/react-native-a11y/utils/get-property.js +9 -0
- package/dist/plugins/react-native-a11y/utils/has-accessible-name.d.ts +6 -0
- package/dist/plugins/react-native-a11y/utils/has-accessible-name.js +27 -0
- package/dist/plugins/react-native-a11y/utils/has-property.d.ts +3 -0
- package/dist/plugins/react-native-a11y/utils/has-property.js +3 -0
- package/dist/plugins/react-native-a11y/utils/has-spread-props.d.ts +8 -0
- package/dist/plugins/react-native-a11y/utils/has-spread-props.js +8 -0
- package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/is-definitely-grouped.js +20 -0
- package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.d.ts +19 -0
- package/dist/plugins/react-native-a11y/utils/is-hidden-from-accessibility.js +51 -0
- package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.d.ts +9 -0
- package/dist/plugins/react-native-a11y/utils/is-inside-focus-stop.js +36 -0
- package/dist/plugins/react-native-a11y/utils/is-interactive.d.ts +16 -0
- package/dist/plugins/react-native-a11y/utils/is-interactive.js +38 -0
- package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.d.ts +7 -0
- package/dist/plugins/react-native-a11y/utils/is-not-a-focus-stop.js +19 -0
- package/dist/react-native.js +85 -1
- package/dist/shared.d.ts +8 -0
- package/dist/shared.js +22 -1
- package/package.json +4 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import { TEXTS } from "../constants/react-native-components.js";
|
|
3
|
+
// ponytail: `Text` by name only. A renamed import or a differently named text
|
|
4
|
+
// atom reads as no text; pass it through `additionalComponents` if that bites.
|
|
5
|
+
const isTextElement = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
|
|
6
|
+
TEXTS.includes(node.openingElement.name.name);
|
|
7
|
+
/** `<Text>{null}</Text>` and `<Text> </Text>` render nothing a user can read. */
|
|
8
|
+
const hasRenderedContent = (child) => {
|
|
9
|
+
if (child.type === AST_NODE_TYPES.JSXText) {
|
|
10
|
+
return child.value.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
13
|
+
const { expression } = child;
|
|
14
|
+
return (expression.type !== AST_NODE_TYPES.JSXEmptyExpression &&
|
|
15
|
+
!(expression.type === AST_NODE_TYPES.Literal && expression.value === null) &&
|
|
16
|
+
!(expression.type === AST_NODE_TYPES.Identifier &&
|
|
17
|
+
expression.name === "undefined"));
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
/** The JSX a conditional can render, so a branch carrying text still counts. */
|
|
22
|
+
const branchesOf = (expression) => {
|
|
23
|
+
if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
24
|
+
return [expression.consequent, expression.alternate];
|
|
25
|
+
}
|
|
26
|
+
if (expression.type === AST_NODE_TYPES.LogicalExpression) {
|
|
27
|
+
return [expression.left, expression.right];
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
};
|
|
31
|
+
const isJsxChild = (expression) => expression.type === AST_NODE_TYPES.JSXElement ||
|
|
32
|
+
expression.type === AST_NODE_TYPES.JSXFragment;
|
|
33
|
+
/**
|
|
34
|
+
* Counts children that render visible text, which is what makes an
|
|
35
|
+
* `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
|
|
36
|
+
* `<Text>{t("Save")}</Text>` is, so neither can be judged on node type alone.
|
|
37
|
+
*/
|
|
38
|
+
const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((amount, child) => {
|
|
39
|
+
if (child.type === AST_NODE_TYPES.JSXText) {
|
|
40
|
+
return child.value.trim().length > 0 ? amount + 1 : amount;
|
|
41
|
+
}
|
|
42
|
+
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
43
|
+
const { expression } = child;
|
|
44
|
+
if (expression.type === AST_NODE_TYPES.Literal ||
|
|
45
|
+
expression.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
46
|
+
return amount + 1;
|
|
47
|
+
}
|
|
48
|
+
// JSX inside an expression renders like any other child, and a branch
|
|
49
|
+
// that renders text still names the control
|
|
50
|
+
const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
|
|
51
|
+
return rendered.length > 0
|
|
52
|
+
? getAmountOfTextChildren(rendered, amount)
|
|
53
|
+
: amount;
|
|
54
|
+
}
|
|
55
|
+
if (child.type === AST_NODE_TYPES.JSXFragment) {
|
|
56
|
+
return getAmountOfTextChildren(child.children, amount);
|
|
57
|
+
}
|
|
58
|
+
if (child.type === AST_NODE_TYPES.JSXElement) {
|
|
59
|
+
// A `Text` renders whatever it is given, including an i18n call
|
|
60
|
+
if (isTextElement(child)) {
|
|
61
|
+
return child.children.some(hasRenderedContent) ? amount + 1 : amount;
|
|
62
|
+
}
|
|
63
|
+
return getAmountOfTextChildren(child.children, amount);
|
|
64
|
+
}
|
|
65
|
+
return amount;
|
|
66
|
+
}, oldAmount);
|
|
67
|
+
export default getAmountOfTextChildren;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RuleContext } from "@typescript-eslint/utils/ts-eslint";
|
|
2
|
+
import type { Options } from "../models/optional-options.js";
|
|
3
|
+
declare const getComponents: <T extends string, P extends Options>({ context, defaultComponents, }: {
|
|
4
|
+
context: RuleContext<T, P>;
|
|
5
|
+
defaultComponents?: string[];
|
|
6
|
+
}) => string[];
|
|
7
|
+
export default getComponents;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const getComponents = ({ context, defaultComponents, }) => {
|
|
2
|
+
const options = context.options.length > 0 ? context.options[0] : {};
|
|
3
|
+
const { additionalComponents } = options;
|
|
4
|
+
// Combine default React Native components with user-defined custom components
|
|
5
|
+
const componentsToCheck = [
|
|
6
|
+
...(defaultComponents ?? []),
|
|
7
|
+
...(additionalComponents ?? []),
|
|
8
|
+
];
|
|
9
|
+
return componentsToCheck;
|
|
10
|
+
};
|
|
11
|
+
export default getComponents;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* The name to match a component by. A qualified name resolves to its last part,
|
|
4
|
+
* since `Animated.Image` is an `Image` wrapped for animation and owes the same
|
|
5
|
+
* accessibility props. A renamed import resolves to the name it was imported
|
|
6
|
+
* under. Returns undefined for a namespaced name (`svg:rect`), which is not a
|
|
7
|
+
* React Native component.
|
|
8
|
+
*/
|
|
9
|
+
declare const getElementName: (node: TSESTree.JSXOpeningElement) => string | undefined;
|
|
10
|
+
export default getElementName;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
/** Local name → imported name, per file, so the walk below runs once. */
|
|
3
|
+
const aliasesByProgram = new WeakMap();
|
|
4
|
+
/** The file's root. Only a Program has no parent, so the walk ends there. */
|
|
5
|
+
const getProgram = (node) => {
|
|
6
|
+
let current = node;
|
|
7
|
+
while (current.parent) {
|
|
8
|
+
current = current.parent;
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Every renamed import in the file. A default import carries no name to match
|
|
14
|
+
* on — `import Img from "expo-image"` says nothing about what `Img` is — so
|
|
15
|
+
* only named specifiers are collected.
|
|
16
|
+
*/
|
|
17
|
+
const collectAliases = (program) => {
|
|
18
|
+
const aliases = new Map();
|
|
19
|
+
for (const statement of program.body) {
|
|
20
|
+
if (statement.type !== AST_NODE_TYPES.ImportDeclaration) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
for (const specifier of statement.specifiers) {
|
|
24
|
+
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (specifier.imported.type !== AST_NODE_TYPES.Identifier) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
aliases.set(specifier.local.name, specifier.imported.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return aliases;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What a local name was imported under, or the name itself when nothing
|
|
37
|
+
* renamed it. `Image as ExpoImage` is still an `Image` and owes the same
|
|
38
|
+
* accessibility props; matching the local name would let a rename opt a
|
|
39
|
+
* component out of every rule in this plugin.
|
|
40
|
+
*/
|
|
41
|
+
const resolveAlias = (node, name) => {
|
|
42
|
+
const program = getProgram(node);
|
|
43
|
+
let aliases = aliasesByProgram.get(program);
|
|
44
|
+
if (aliases === undefined) {
|
|
45
|
+
aliases = collectAliases(program);
|
|
46
|
+
aliasesByProgram.set(program, aliases);
|
|
47
|
+
}
|
|
48
|
+
return aliases.get(name) ?? name;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The name to match a component by. A qualified name resolves to its last part,
|
|
52
|
+
* since `Animated.Image` is an `Image` wrapped for animation and owes the same
|
|
53
|
+
* accessibility props. A renamed import resolves to the name it was imported
|
|
54
|
+
* under. Returns undefined for a namespaced name (`svg:rect`), which is not a
|
|
55
|
+
* React Native component.
|
|
56
|
+
*/
|
|
57
|
+
const getElementName = (node) => {
|
|
58
|
+
if (node.name.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
59
|
+
return resolveAlias(node, node.name.name);
|
|
60
|
+
}
|
|
61
|
+
if (node.name.type === AST_NODE_TYPES.JSXMemberExpression) {
|
|
62
|
+
return node.name.property.name;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
};
|
|
66
|
+
export default getElementName;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
const isJsx = (node) => node.type === AST_NODE_TYPES.JSXElement ||
|
|
3
|
+
node.type === AST_NODE_TYPES.JSXFragment;
|
|
4
|
+
/** The JSX an expression can render, including through a conditional. */
|
|
5
|
+
const renderedBy = (expression) => {
|
|
6
|
+
if (isJsx(expression)) {
|
|
7
|
+
return [expression];
|
|
8
|
+
}
|
|
9
|
+
if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
10
|
+
return [
|
|
11
|
+
...renderedBy(expression.consequent),
|
|
12
|
+
...renderedBy(expression.alternate),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
if (expression.type === AST_NODE_TYPES.LogicalExpression) {
|
|
16
|
+
return [...renderedBy(expression.left), ...renderedBy(expression.right)];
|
|
17
|
+
}
|
|
18
|
+
// `items.map(() => <Row />)` renders its callback's JSX
|
|
19
|
+
if (expression.type === AST_NODE_TYPES.CallExpression) {
|
|
20
|
+
return expression.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
21
|
+
argument.body.type !== AST_NODE_TYPES.BlockStatement
|
|
22
|
+
? renderedBy(argument.body)
|
|
23
|
+
: []);
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
};
|
|
27
|
+
const collect = (children, found) => {
|
|
28
|
+
for (const child of children) {
|
|
29
|
+
if (child.type === AST_NODE_TYPES.JSXElement) {
|
|
30
|
+
found.push(child.openingElement);
|
|
31
|
+
collect(child.children, found);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (child.type === AST_NODE_TYPES.JSXFragment) {
|
|
35
|
+
collect(child.children, found);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
39
|
+
collect(renderedBy(child.expression), found);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** Every JSX element rendered below this one, at any depth. */
|
|
44
|
+
const getJsxDescendants = (node) => {
|
|
45
|
+
const found = [];
|
|
46
|
+
collect(node.parent.children, found);
|
|
47
|
+
return found;
|
|
48
|
+
};
|
|
49
|
+
export default getJsxDescendants;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* The literal value of a prop, or `undefined` when it is absent or computed.
|
|
4
|
+
* A computed value cannot be judged statically, so callers skip it.
|
|
5
|
+
*/
|
|
6
|
+
declare const getLiteralPropertyValue: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => TSESTree.Literal["value"] | undefined;
|
|
7
|
+
export default getLiteralPropertyValue;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import getProperty from "./get-property.js";
|
|
3
|
+
/**
|
|
4
|
+
* The literal value of a prop, or `undefined` when it is absent or computed.
|
|
5
|
+
* A computed value cannot be judged statically, so callers skip it.
|
|
6
|
+
*/
|
|
7
|
+
const getLiteralPropertyValue = (node, propertyNames) => {
|
|
8
|
+
const attribute = getProperty(node, propertyNames);
|
|
9
|
+
if (!attribute?.value) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (attribute.value.type === AST_NODE_TYPES.Literal) {
|
|
13
|
+
return attribute.value.value;
|
|
14
|
+
}
|
|
15
|
+
if (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
16
|
+
attribute.value.expression.type === AST_NODE_TYPES.Literal) {
|
|
17
|
+
return attribute.value.expression.value;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|
|
21
|
+
export default getLiteralPropertyValue;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* The visible text a node renders, where every part of it is a plain literal.
|
|
4
|
+
* Anything resolved at runtime — an i18n call, an interpolation — reads as
|
|
5
|
+
* undefined, since a substring check against it would be guesswork.
|
|
6
|
+
*/
|
|
7
|
+
declare const getLiteralTextChild: (children: TSESTree.JSXChild[]) => string | undefined;
|
|
8
|
+
export default getLiteralTextChild;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import { TEXTS } from "../constants/react-native-components.js";
|
|
3
|
+
/** A childless non-text element renders no words of its own. */
|
|
4
|
+
const hasNoWords = (node) => node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier &&
|
|
5
|
+
!TEXTS.includes(node.openingElement.name.name) &&
|
|
6
|
+
node.children.length === 0;
|
|
7
|
+
/** The literal words in these children, or undefined if any part is computed. */
|
|
8
|
+
const wordsIn = (children) => children.reduce((words, child) => {
|
|
9
|
+
if (!words) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (child.type === AST_NODE_TYPES.JSXText) {
|
|
13
|
+
const text = child.value.trim();
|
|
14
|
+
return text.length > 0 ? [...words, text] : words;
|
|
15
|
+
}
|
|
16
|
+
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
17
|
+
// A value only known at runtime cannot be compared against
|
|
18
|
+
return child.expression.type === AST_NODE_TYPES.Literal
|
|
19
|
+
? [...words, String(child.expression.value)]
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
if (child.type === AST_NODE_TYPES.JSXElement) {
|
|
23
|
+
if (hasNoWords(child)) {
|
|
24
|
+
return words;
|
|
25
|
+
}
|
|
26
|
+
const nested = wordsIn(child.children);
|
|
27
|
+
return nested ? [...words, ...nested] : undefined;
|
|
28
|
+
}
|
|
29
|
+
return words;
|
|
30
|
+
}, []);
|
|
31
|
+
/**
|
|
32
|
+
* The visible text a node renders, where every part of it is a plain literal.
|
|
33
|
+
* Anything resolved at runtime — an i18n call, an interpolation — reads as
|
|
34
|
+
* undefined, since a substring check against it would be guesswork.
|
|
35
|
+
*/
|
|
36
|
+
const getLiteralTextChild = (children) => {
|
|
37
|
+
const words = wordsIn(children);
|
|
38
|
+
return words && words.length > 0 ? words.join(" ") : undefined;
|
|
39
|
+
};
|
|
40
|
+
export default getLiteralTextChild;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/** The first attribute on `node` matching one of `propNames`, if any. */
|
|
3
|
+
declare const getProperty: (node: TSESTree.JSXOpeningElement, propertyNames: string | string[]) => TSESTree.JSXAttribute | undefined;
|
|
4
|
+
export default getProperty;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
/** The first attribute on `node` matching one of `propNames`, if any. */
|
|
3
|
+
const getProperty = (node, propertyNames) => {
|
|
4
|
+
const names = Array.isArray(propertyNames) ? propertyNames : [propertyNames];
|
|
5
|
+
return node.attributes.find((attribute) => attribute.type === AST_NODE_TYPES.JSXAttribute &&
|
|
6
|
+
attribute.name.type === AST_NODE_TYPES.JSXIdentifier &&
|
|
7
|
+
names.includes(attribute.name.name));
|
|
8
|
+
};
|
|
9
|
+
export default getProperty;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/** An `accessibilityLabel=""` clears the name rather than setting one. */
|
|
3
|
+
export declare const hasEmptyLabel: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
4
|
+
/** The label's text where it is a plain literal, otherwise undefined. */
|
|
5
|
+
export declare const getLiteralLabel: (node: TSESTree.JSXOpeningElement) => string | undefined;
|
|
6
|
+
export declare const hasLabelProperty: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import getLiteralPropertyValue from "./get-literal-property-value.js";
|
|
3
|
+
import getProperty from "./get-property.js";
|
|
4
|
+
const LABEL_PROPS = ["accessibilityLabel", "aria-label"];
|
|
5
|
+
/** An `accessibilityLabel=""` clears the name rather than setting one. */
|
|
6
|
+
export const hasEmptyLabel = (node) => {
|
|
7
|
+
const attribute = getProperty(node, LABEL_PROPS);
|
|
8
|
+
if (!attribute?.value) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (attribute.value.type === AST_NODE_TYPES.Literal) {
|
|
12
|
+
return attribute.value.value === "";
|
|
13
|
+
}
|
|
14
|
+
if (attribute.value.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const { expression } = attribute.value;
|
|
18
|
+
return expression.type === AST_NODE_TYPES.Literal ? expression.value === "" : (expression.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
19
|
+
expression.expressions.length === 0 &&
|
|
20
|
+
expression.quasis.every((quasi) => quasi.value.raw === ""));
|
|
21
|
+
};
|
|
22
|
+
/** The label's text where it is a plain literal, otherwise undefined. */
|
|
23
|
+
export const getLiteralLabel = (node) => {
|
|
24
|
+
const value = getLiteralPropertyValue(node, LABEL_PROPS);
|
|
25
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
26
|
+
};
|
|
27
|
+
export const hasLabelProperty = (node) => getProperty(node, LABEL_PROPS) !== undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* A node spreading unknown props may already carry the accessibility prop a
|
|
4
|
+
* rule is looking for. Wrapper components pass them down this way constantly,
|
|
5
|
+
* so reporting one is a false positive rather than a finding.
|
|
6
|
+
*/
|
|
7
|
+
declare const hasSpreadProps: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
8
|
+
export default hasSpreadProps;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* A node spreading unknown props may already carry the accessibility prop a
|
|
4
|
+
* rule is looking for. Wrapper components pass them down this way constantly,
|
|
5
|
+
* so reporting one is a false positive rather than a finding.
|
|
6
|
+
*/
|
|
7
|
+
const hasSpreadProps = (node) => node.attributes.some((attribute) => attribute.type === AST_NODE_TYPES.JSXSpreadAttribute);
|
|
8
|
+
export default hasSpreadProps;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the node is collapsed into one focus stop for certain. Reporting
|
|
4
|
+
* needs that certainty: `accessible={isGrouped}` may well be false at runtime.
|
|
5
|
+
*/
|
|
6
|
+
declare const isDefinitelyGrouped: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
7
|
+
export default isDefinitelyGrouped;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import getProperty from "./get-property.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether the node is collapsed into one focus stop for certain. Reporting
|
|
5
|
+
* needs that certainty: `accessible={isGrouped}` may well be false at runtime.
|
|
6
|
+
*/
|
|
7
|
+
const isDefinitelyGrouped = (node) => {
|
|
8
|
+
const attribute = getProperty(node, "accessible");
|
|
9
|
+
if (!attribute) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
// A bare `accessible` is `true`
|
|
13
|
+
if (!attribute.value) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
17
|
+
attribute.value.expression.type === AST_NODE_TYPES.Literal &&
|
|
18
|
+
attribute.value.expression.value === true);
|
|
19
|
+
};
|
|
20
|
+
export default isDefinitelyGrouped;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* Whether this node hides everything below it. `accessible={false}` does not:
|
|
4
|
+
* it drops the node as a focus stop and leaves its children reachable.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isHidingSubtree: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the node is hidden for certain. Reporting a node needs this
|
|
9
|
+
* certainty, since a value computed at runtime may well be `false`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const isDefinitelyHidden: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Decorative content is hidden from assistive tech rather than labelled.
|
|
14
|
+
*
|
|
15
|
+
* A value computed at runtime counts as hidden here: suppressing a report on a
|
|
16
|
+
* node that may be hidden beats reporting one that is.
|
|
17
|
+
*/
|
|
18
|
+
declare const isHiddenFromAccessibility: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
19
|
+
export default isHiddenFromAccessibility;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import { HIDDEN_PROPS } from "../constants/accessibility-props.js";
|
|
3
|
+
import getProperty from "./get-property.js";
|
|
4
|
+
const literalValue = (attribute) => {
|
|
5
|
+
// A bare `aria-hidden` is `true`
|
|
6
|
+
if (!attribute.value) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (attribute.value.type === AST_NODE_TYPES.Literal) {
|
|
10
|
+
return attribute.value.value;
|
|
11
|
+
}
|
|
12
|
+
if (attribute.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
13
|
+
attribute.value.expression.type === AST_NODE_TYPES.Literal) {
|
|
14
|
+
return attribute.value.expression.value;
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
};
|
|
18
|
+
const isHidingDescendants = (node) => {
|
|
19
|
+
const attribute = getProperty(node, "importantForAccessibility");
|
|
20
|
+
return attribute !== undefined && literalValue(attribute) === "no-hide-descendants";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Whether this node hides everything below it. `accessible={false}` does not:
|
|
24
|
+
* it drops the node as a focus stop and leaves its children reachable.
|
|
25
|
+
*/
|
|
26
|
+
export const isHidingSubtree = (node) => {
|
|
27
|
+
const hidden = getProperty(node, HIDDEN_PROPS);
|
|
28
|
+
return ((hidden !== undefined && literalValue(hidden) !== false) ||
|
|
29
|
+
isHidingDescendants(node));
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Whether the node is hidden for certain. Reporting a node needs this
|
|
33
|
+
* certainty, since a value computed at runtime may well be `false`.
|
|
34
|
+
*/
|
|
35
|
+
export const isDefinitelyHidden = (node) => {
|
|
36
|
+
const hidden = getProperty(node, HIDDEN_PROPS);
|
|
37
|
+
return ((hidden !== undefined && literalValue(hidden) === true) ||
|
|
38
|
+
isHidingDescendants(node));
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Decorative content is hidden from assistive tech rather than labelled.
|
|
42
|
+
*
|
|
43
|
+
* A value computed at runtime counts as hidden here: suppressing a report on a
|
|
44
|
+
* node that may be hidden beats reporting one that is.
|
|
45
|
+
*/
|
|
46
|
+
const isHiddenFromAccessibility = (node) => {
|
|
47
|
+
const hidden = getProperty(node, HIDDEN_PROPS);
|
|
48
|
+
return ((hidden !== undefined && literalValue(hidden) !== false) ||
|
|
49
|
+
isHidingDescendants(node));
|
|
50
|
+
};
|
|
51
|
+
export default isHiddenFromAccessibility;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* Whether an ancestor already answers for this node: a touchable is one focus
|
|
4
|
+
* stop, so its children are not stepped through separately and the role and
|
|
5
|
+
* label belong on the surface rather than on every child. The same holds for a
|
|
6
|
+
* composite control grouped with `accessible`, and for a hidden subtree.
|
|
7
|
+
*/
|
|
8
|
+
declare const isInsideFocusStop: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
9
|
+
export default isInsideFocusStop;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import { TOUCHABLES } from "../constants/react-native-components.js";
|
|
3
|
+
import getElementName from "./get-element-name.js";
|
|
4
|
+
import getProperty from "./get-property.js";
|
|
5
|
+
import { isHidingSubtree } from "./is-hidden-from-accessibility.js";
|
|
6
|
+
const jsxAncestors = (node) => {
|
|
7
|
+
const ancestors = [];
|
|
8
|
+
// The node's own JSXElement is `node.parent`, so ancestry starts above it
|
|
9
|
+
let current = node.parent.parent;
|
|
10
|
+
while (current) {
|
|
11
|
+
if (current.type === AST_NODE_TYPES.JSXElement) {
|
|
12
|
+
ancestors.push(current.openingElement);
|
|
13
|
+
}
|
|
14
|
+
current = current.parent;
|
|
15
|
+
}
|
|
16
|
+
return ancestors;
|
|
17
|
+
};
|
|
18
|
+
/** A composite control marked as one focus stop carries the name for all of it. */
|
|
19
|
+
const isGrouped = (node) => {
|
|
20
|
+
const accessible = getProperty(node, "accessible");
|
|
21
|
+
return accessible !== undefined && !(accessible.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
22
|
+
accessible.value.expression.type === AST_NODE_TYPES.Literal &&
|
|
23
|
+
accessible.value.expression.value === false);
|
|
24
|
+
};
|
|
25
|
+
const isTouchable = (node) => {
|
|
26
|
+
const name = getElementName(node);
|
|
27
|
+
return name !== undefined && TOUCHABLES.includes(name);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Whether an ancestor already answers for this node: a touchable is one focus
|
|
31
|
+
* stop, so its children are not stepped through separately and the role and
|
|
32
|
+
* label belong on the surface rather than on every child. The same holds for a
|
|
33
|
+
* composite control grouped with `accessible`, and for a hidden subtree.
|
|
34
|
+
*/
|
|
35
|
+
const isInsideFocusStop = (node) => jsxAncestors(node).some((ancestor) => isTouchable(ancestor) || isGrouped(ancestor) || isHidingSubtree(ancestor));
|
|
36
|
+
export default isInsideFocusStop;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* `accessible` makes the node a single focus stop. It defaults to true on the
|
|
4
|
+
* touchables and false elsewhere, so only an explicit prop is read here.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isMarkedAccessible: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
7
|
+
export declare const hasInteractiveRole: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
8
|
+
export declare const hasTouchHandler: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* A node a user can operate: an RN host (or a wrapper listed as one) that
|
|
11
|
+
* announces itself as a control, handles a touch, or has been collapsed into
|
|
12
|
+
* one focus stop on purpose. An unlisted wrapper's `onPress` / `role` /
|
|
13
|
+
* `accessible` are its own API, not a sign that this JSX node is the control.
|
|
14
|
+
*/
|
|
15
|
+
declare const isInteractive: (node: TSESTree.JSXOpeningElement, extraHosts?: string[]) => boolean;
|
|
16
|
+
export default isInteractive;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import { INTERACTIVE_ROLES, TOUCH_HANDLER_PROPS } from "../constants/interactive.js";
|
|
3
|
+
import { IMAGES, INPUTS, TEXTS, TOUCHABLES, } from "../constants/react-native-components.js";
|
|
4
|
+
import getElementName from "./get-element-name.js";
|
|
5
|
+
import getLiteralPropertyValue from "./get-literal-property-value.js";
|
|
6
|
+
import getProperty from "./get-property.js";
|
|
7
|
+
import hasProperty from "./has-property.js";
|
|
8
|
+
const RN_HOSTS = new Set(["View", ...TOUCHABLES, ...TEXTS, ...IMAGES, ...INPUTS]);
|
|
9
|
+
/**
|
|
10
|
+
* `accessible` makes the node a single focus stop. It defaults to true on the
|
|
11
|
+
* touchables and false elsewhere, so only an explicit prop is read here.
|
|
12
|
+
*/
|
|
13
|
+
export const isMarkedAccessible = (node) => {
|
|
14
|
+
const attribute = getProperty(node, "accessible");
|
|
15
|
+
return attribute !== undefined && !(attribute.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
16
|
+
attribute.value.expression.type === AST_NODE_TYPES.Literal &&
|
|
17
|
+
attribute.value.expression.value === false);
|
|
18
|
+
};
|
|
19
|
+
export const hasInteractiveRole = (node) => {
|
|
20
|
+
const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
|
|
21
|
+
return typeof role === "string" && INTERACTIVE_ROLES.includes(role);
|
|
22
|
+
};
|
|
23
|
+
export const hasTouchHandler = (node) => hasProperty(node, TOUCH_HANDLER_PROPS);
|
|
24
|
+
const isHost = (node, extraHosts) => {
|
|
25
|
+
const name = getElementName(node);
|
|
26
|
+
return (name !== undefined && (RN_HOSTS.has(name) || extraHosts.includes(name)));
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A node a user can operate: an RN host (or a wrapper listed as one) that
|
|
30
|
+
* announces itself as a control, handles a touch, or has been collapsed into
|
|
31
|
+
* one focus stop on purpose. An unlisted wrapper's `onPress` / `role` /
|
|
32
|
+
* `accessible` are its own API, not a sign that this JSX node is the control.
|
|
33
|
+
*/
|
|
34
|
+
const isInteractive = (node, extraHosts = []) => isHost(node, extraHosts) &&
|
|
35
|
+
(isMarkedAccessible(node) ||
|
|
36
|
+
hasInteractiveRole(node) ||
|
|
37
|
+
hasTouchHandler(node));
|
|
38
|
+
export default isInteractive;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
/**
|
|
3
|
+
* `accessible={false}` only opts a touchable out of being a focus stop. On an
|
|
4
|
+
* `Image` or `View` it is not a hide, and cannot excuse a missing name or role.
|
|
5
|
+
*/
|
|
6
|
+
declare const isNotAFocusStop: (node: TSESTree.JSXOpeningElement) => boolean;
|
|
7
|
+
export default isNotAFocusStop;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import { TOUCHABLES } from "../constants/react-native-components.js";
|
|
3
|
+
import getElementName from "./get-element-name.js";
|
|
4
|
+
import getProperty from "./get-property.js";
|
|
5
|
+
/**
|
|
6
|
+
* `accessible={false}` only opts a touchable out of being a focus stop. On an
|
|
7
|
+
* `Image` or `View` it is not a hide, and cannot excuse a missing name or role.
|
|
8
|
+
*/
|
|
9
|
+
const isNotAFocusStop = (node) => {
|
|
10
|
+
const name = getElementName(node);
|
|
11
|
+
if (name === undefined || !TOUCHABLES.includes(name)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const attribute = getProperty(node, "accessible");
|
|
15
|
+
return (attribute?.value?.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
16
|
+
attribute.value.expression.type === AST_NODE_TYPES.Literal &&
|
|
17
|
+
attribute.value.expression.value === false);
|
|
18
|
+
};
|
|
19
|
+
export default isNotAFocusStop;
|