feat(lint): useAnchorContent for HTML
ADD
internal/compiler/lint/rules/a11y/useAnchorContent.test.rjson
+[
+ {
+ filename: "file.jsx"
+ invalid: [
+ "<a />"
+ "<a><TextWrapper aria-hidden /></a>"
+ ]
+ valid: [
+ "<a>Anchor Content!</a>"
+ "<a><TextWrapper /></a>"
+ "<a dangerouslySetInnerHTML={{ __html: 'foo' }} />"
+ "<a><TextWrapper aria-hidden={true} /> visible content</a>"
+ ]
+}
+ {
+ filename: "file.html"
+ invalid: [
+ "<a></a>"
+ ]
+ valid: [
+ "<a>Anchor Content!</a>"
+ "<a><div aria-hidden=\"true\"></div>visible content</a>"
+ ]
+}
+]

ADD
internal/compiler/lint/rules/a11y/useAnchorContent.ts
+import {createVisitor, signals} from "@internal/compiler";
+import {HTMLElement, JSXElement} from "@internal/ast";
+import {descriptions} from "@internal/diagnostics";
+import {hasJSXAttribute, isJSXElement} from "@internal/js-ast-utils";
+import isHTMLElement from "@internal/js-ast-utils/isHTMLElement";
+import hasHTMLAttribute from "@internal/js-ast-utils/hasHTMLAttribute";
+
+function hasAnchorContent(node: JSXElement): boolean {
+ return (
+ hasJSXAttribute(node, "dangerouslySetInnerHTML") ||
+ (node.children.length > 0 &&
+ node.children.some((child) =>
+ child.type !== "JSXElement" || !hasJSXAttribute(child, "aria-hidden")
+ ))
+ );
+}
+
+function htmlHasAnchorContent(node: HTMLElement) {
+ return node.children.some((child) =>
+ child.type !== "HTMLElement" || !hasHTMLAttribute(child, "aria-hidden")
+ );
+}
+
+export default createVisitor({
+ name: "a11y/useAnchorContent",
+ enter(path) {
+ const {node} = path;
+
+ if (isHTMLElement(node) && node.name.name === "a") {
+ if (!htmlHasAnchorContent(node)) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.A11_Y_USE_ANCHOR_CONTENT,
+ );
+ }
+ } else if (isJSXElement(node, "a") && !hasAnchorContent(node)) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.A11_Y_USE_ANCHOR_CONTENT,
+ );
+ }
+
+ return signals.retain;
+ },
+});

MODIFY
internal/compiler/lint/rules/index.ts
internal/compiler/lint/rules/index.ts
-/* GENERATED:START(hash:ad0a112eefabd76e65b95091fe6392a3bffd4543,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:63a3051e3b0702d483cf8272db0037c8f21a4f33,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
import noAccessKey from "./a11y/noAccessKey";
import noAriaUnsupportedElements from "./a11y/noAriaUnsupportedElements";
import noAutofocus from "./a11y/noAutofocus";
@@ -11,6 +11,7 @@ import noRedundantAlt from "./a11y/noRedundantAlt";
import noSvgWithoutTitle from "./a11y/noSvgWithoutTitle";
import noTargetBlank from "./a11y/noTargetBlank";
import useAltText from "./a11y/useAltText";
+import useAnchorContent from "./a11y/useAnchorContent";
import useAriaProps from "./a11y/useAriaProps";
import useAriaProptypes from "./a11y/useAriaProptypes";
import useHtmlLang from "./a11y/useHtmlLang";
@@ -70,7 +71,6 @@ import useTemplate from "./js/useTemplate";
import useWhile from "./js/useWhile";
import noOnChange from "./jsx-a11y/noOnChange";
import noRedundantRoles from "./jsx-a11y/noRedundantRoles";
-import useAnchorContent from "./jsx-a11y/useAnchorContent";
import useAriaPropsForRole from "./jsx-a11y/useAriaPropsForRole";
import useHeadingContent from "./jsx-a11y/useHeadingContent";
import useKeyWithClickEvents from "./jsx-a11y/useKeyWithClickEvents";
@@ -134,6 +134,7 @@ lintTransforms.set("a11y/noRedundantAlt", noRedundantAlt);
lintTransforms.set("a11y/noSvgWithoutTitle", noSvgWithoutTitle);
lintTransforms.set("a11y/noTargetBlank", noTargetBlank);
lintTransforms.set("a11y/useAltText", useAltText);
+lintTransforms.set("a11y/useAnchorContent", useAnchorContent);
lintTransforms.set("a11y/useAriaProps", useAriaProps);
lintTransforms.set("a11y/useAriaProptypes", useAriaProptypes);
lintTransforms.set("a11y/useHtmlLang", useHtmlLang);
@@ -199,7 +200,6 @@ lintTransforms.set("js/useTemplate", useTemplate);
lintTransforms.set("js/useWhile", useWhile);
lintTransforms.set("jsx-a11y/noOnChange", noOnChange);
lintTransforms.set("jsx-a11y/noRedundantRoles", noRedundantRoles);
-lintTransforms.set("jsx-a11y/useAnchorContent", useAnchorContent);
lintTransforms.set("jsx-a11y/useAriaPropsForRole", useAriaPropsForRole);
lintTransforms.set("jsx-a11y/useHeadingContent", useHeadingContent);
lintTransforms.set("jsx-a11y/useKeyWithClickEvents", useKeyWithClickEvents);
@@ -280,6 +280,7 @@ export const lintRuleNames: LintRuleName[] = [
"a11y/noSvgWithoutTitle",
"a11y/noTargetBlank",
"a11y/useAltText",
+ "a11y/useAnchorContent",
"a11y/useAriaProps",
"a11y/useAriaProptypes",
"a11y/useHtmlLang",
@@ -339,7 +340,6 @@ export const lintRuleNames: LintRuleName[] = [
"js/useWhile",
"jsx-a11y/noOnChange",
"jsx-a11y/noRedundantRoles",
- "jsx-a11y/useAnchorContent",
"jsx-a11y/useAriaPropsForRole",
"jsx-a11y/useHeadingContent",
"jsx-a11y/useKeyWithClickEvents",
@@ -400,6 +400,7 @@ export type LintRuleName =
| "a11y/noSvgWithoutTitle"
| "a11y/noTargetBlank"
| "a11y/useAltText"
+ | "a11y/useAnchorContent"
| "a11y/useAriaProps"
| "a11y/useAriaProptypes"
| "a11y/useHtmlLang"
@@ -459,7 +460,6 @@ export type LintRuleName =
| "js/useWhile"
| "jsx-a11y/noOnChange"
| "jsx-a11y/noRedundantRoles"
- | "jsx-a11y/useAnchorContent"
| "jsx-a11y/useAriaPropsForRole"
| "jsx-a11y/useHeadingContent"
| "jsx-a11y/useKeyWithClickEvents"

MODIFY
internal/compiler/lint/rules/jsx-a11y/noRedundantRoles.test.rjson
internal/compiler/lint/rules/jsx-a11y/noRedundantRoles.test.rjson
@@ -12,7 +12,7 @@ invalid: [
"<td role='gridcell'></td>"
"<fieldset role='group'></fieldset>"
"<img src='foo' alt='bar' role='img' />"
- "<a role='link'></a>"
+ "<a role='link'>something</a>"
"<link role='link' />"
"<ol role='list' ></ol>"
"<ul role='list' ></ul>"

DELETE
internal/compiler/lint/rules/jsx-a11y/useAnchorContent.test.rjson
-filename: "file.jsx"
-invalid: [
- "<a />"
- "<a><TextWrapper aria-hidden /></a>"
-]
-valid: [
- "<a>Anchor Content!</a>"
- "<a><TextWrapper /></a>"
- "<a dangerouslySetInnerHTML={{ __html: 'foo' }} />"
- "<a><TextWrapper aria-hidden={true} /> visible content</a>"
-]

DELETE
internal/compiler/lint/rules/jsx-a11y/useAnchorContent.ts
-import {createVisitor, signals} from "@internal/compiler";
-import {JSXElement} from "@internal/ast";
-import {descriptions} from "@internal/diagnostics";
-import {hasJSXAttribute, isJSXElement} from "@internal/js-ast-utils";
-
-function hasAnchorContent(node: JSXElement): boolean {
- return (
- hasJSXAttribute(node, "dangerouslySetInnerHTML") ||
- (node.children.length > 0 &&
- node.children.some((child) =>
- child.type !== "JSXElement" || !hasJSXAttribute(child, "aria-hidden")
- ))
- );
-}
-
-export default createVisitor({
- name: "jsx-a11y/useAnchorContent",
- enter(path) {
- const {node} = path;
-
- if (isJSXElement(node, "a") && !hasAnchorContent(node)) {
- path.context.addNodeDiagnostic(
- node,
- descriptions.LINT.JSX_A11Y_ANCHOR_HAS_CONTENT,
- );
- }
-
- return signals.retain;
- },
-});

MODIFY
internal/compiler/lint/rules/tests.ts
internal/compiler/lint/rules/tests.ts
@@ -12,7 +12,7 @@ type Tests = Dict<{
cases: Test | Test[] | Test[][];
}>;
-/* GENERATED:START(hash:ddbc9902ac1dfe455aa1bc9779e45a3fd0038c8c,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:d1cbe4d867d1e592c0c1d92d8ee23f1f75db7a6a,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
// @ts-ignore
import noAccessKey from "./a11y/noAccessKey.test.rjson";
// @ts-ignore
@@ -38,6 +38,8 @@ import noTargetBlank from "./a11y/noTargetBlank.test.rjson";
// @ts-ignore
import useAltText from "./a11y/useAltText.test.rjson";
// @ts-ignore
+import useAnchorContent from "./a11y/useAnchorContent.test.rjson";
+// @ts-ignore
import useAriaProps from "./a11y/useAriaProps.test.rjson";
// @ts-ignore
import useAriaProptypes from "./a11y/useAriaProptypes.test.rjson";
@@ -154,8 +156,6 @@ import noOnChange from "./jsx-a11y/noOnChange.test.rjson";
// @ts-ignore
import noRedundantRoles from "./jsx-a11y/noRedundantRoles.test.rjson";
// @ts-ignore
-import useAnchorContent from "./jsx-a11y/useAnchorContent.test.rjson";
-// @ts-ignore
import useAriaPropsForRole from "./jsx-a11y/useAriaPropsForRole.test.rjson";
// @ts-ignore
import useHeadingContent from "./jsx-a11y/useHeadingContent.test.rjson";
@@ -287,6 +287,10 @@ export const tests: Tests = {
category: ["lint", "a11y", "useAltText"],
cases: useAltText,
},
+ "a11y/useAnchorContent": {
+ category: ["lint", "a11y", "useAnchorContent"],
+ cases: useAnchorContent,
+ },
"a11y/useAriaProps": {
category: ["lint", "a11y", "useAriaProps"],
cases: useAriaProps,
@@ -519,10 +523,6 @@ export const tests: Tests = {
category: ["lint", "jsx-a11y", "noRedundantRoles"],
cases: noRedundantRoles,
},
- "jsx-a11y/useAnchorContent": {
- category: ["lint", "jsx-a11y", "useAnchorContent"],
- cases: useAnchorContent,
- },
"jsx-a11y/useAriaPropsForRole": {
category: ["lint", "jsx-a11y", "useAriaPropsForRole"],
cases: useAriaPropsForRole,

MODIFY
internal/diagnostics/categories.ts
internal/diagnostics/categories.ts
@@ -103,7 +103,7 @@ type DiagnosticCategoryString = StringConverter<DiagnosticCategory>;
type DiagnosticLintCategoryString = StringConverter<DiagnosticLintCategory>;
-/* GENERATED:START(hash:5cb998bcabdf0276ce28aa305c2d8ed6d059f2bb,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:65d9f976508bd6c81aee870aca199c3a09b45b8f,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
export type DiagnosticLintCategory =
| ["lint", "a11y", "noAccessKey"]
| ["lint", "a11y", "noAriaUnsupportedElements"]
@@ -117,6 +117,7 @@ export type DiagnosticLintCategory =
| ["lint", "a11y", "noSvgWithoutTitle"]
| ["lint", "a11y", "noTargetBlank"]
| ["lint", "a11y", "useAltText"]
+ | ["lint", "a11y", "useAnchorContent"]
| ["lint", "a11y", "useAriaProps"]
| ["lint", "a11y", "useAriaProptypes"]
| ["lint", "a11y", "useHtmlLang"]
@@ -176,7 +177,6 @@ export type DiagnosticLintCategory =
| ["lint", "js", "useWhile"]
| ["lint", "jsx-a11y", "noOnChange"]
| ["lint", "jsx-a11y", "noRedundantRoles"]
- | ["lint", "jsx-a11y", "useAnchorContent"]
| ["lint", "jsx-a11y", "useAriaPropsForRole"]
| ["lint", "jsx-a11y", "useHeadingContent"]
| ["lint", "jsx-a11y", "useKeyWithClickEvents"]
@@ -249,6 +249,7 @@ const lintCategoryNameMap: {
"lint/a11y/noSvgWithoutTitle": ["lint", "a11y", "noSvgWithoutTitle"],
"lint/a11y/noTargetBlank": ["lint", "a11y", "noTargetBlank"],
"lint/a11y/useAltText": ["lint", "a11y", "useAltText"],
+ "lint/a11y/useAnchorContent": ["lint", "a11y", "useAnchorContent"],
"lint/a11y/useAriaProps": ["lint", "a11y", "useAriaProps"],
"lint/a11y/useAriaProptypes": ["lint", "a11y", "useAriaProptypes"],
"lint/a11y/useHtmlLang": ["lint", "a11y", "useHtmlLang"],
@@ -320,7 +321,6 @@ const lintCategoryNameMap: {
"lint/js/useWhile": ["lint", "js", "useWhile"],
"lint/jsx-a11y/noOnChange": ["lint", "jsx-a11y", "noOnChange"],
"lint/jsx-a11y/noRedundantRoles": ["lint", "jsx-a11y", "noRedundantRoles"],
- "lint/jsx-a11y/useAnchorContent": ["lint", "jsx-a11y", "useAnchorContent"],
"lint/jsx-a11y/useAriaPropsForRole": [
"lint",
"jsx-a11y",

MODIFY
internal/diagnostics/descriptions/lint.ts
internal/diagnostics/descriptions/lint.ts
@@ -17,6 +17,17 @@ import {addEmphasis, createDiagnosticsCategory, orJoin} from "./index";
import {DIAGNOSTIC_CATEGORIES} from "../categories";
export const lint = createDiagnosticsCategory({
+ A11_Y_USE_ANCHOR_CONTENT: {
+ category: DIAGNOSTIC_CATEGORIES["lint/a11y/useAnchorContent"],
+ message: markup`Provide screen reader accessible content when using <emphasis>anchor</emphasis> elements.`,
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: markup`All links on a page should have content that is accessible to screen readers.`,
+ },
+ ],
+ },
A11_Y_NO_TARGET_BLANK: {
category: DIAGNOSTIC_CATEGORIES["lint/a11y/noTargetBlank"],
message: markup`Avoid using <emphasis>target="_blank"</emphasis> without <emphasis>rel="noreferrer"</emphasis>.`,
@@ -448,17 +459,6 @@ export const lint = createDiagnosticsCategory({
},
],
},
- JSX_A11Y_ANCHOR_HAS_CONTENT: {
- category: DIAGNOSTIC_CATEGORIES["lint/jsx-a11y/useAnchorContent"],
- message: markup`Provide screen reader accessible content when using <emphasis>anchor</emphasis> elements.`,
- advice: [
- {
- type: "log",
- category: "info",
- text: markup`All links on a page should have content that is accessible to screen readers.`,
- },
- ],
- },
A11Y_LANG: (value: string, suggestions: string[]) => ({
category: DIAGNOSTIC_CATEGORIES["lint/a11y/useValidLang"],
message: markup`Provide a valid value for the <emphasis>lang</emphasis> attribute.`,

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -11,7 +11,7 @@ eleventyNavigation:
# Rules
-<!-- GENERATED:START(hash:33cb81b4e653bf1b8a9dfbd6cf9da19374103493,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. -->
+<!-- GENERATED:START(hash:4e7bf9f2dfa22db7b67605fb4b988223d35242d0,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. -->
<section>
<h2>JavaScript</h2>
<p>Rule semantics and descriptions taken from <a href="https://eslint.org/">ESLint</a>. See individual rule docs for direct references.</p>
@@ -472,6 +472,13 @@ Prevent usage of unsafe `target=&quot;_blank&quot;`
Checks that images have a valid alternative text
</div>
<div class="rule">
+<h3 data-toc-exclude id="useAnchorContent">
+ <a href="/docs/lint/rules/a11y/useAnchorContent">useAnchorContent</a>
+ <a class="header-anchor" href="#useAnchorContent"></a>
+</h3>
+MISSING DOCUMENTATION
+</div>
+<div class="rule">
<h3 data-toc-exclude id="useAriaProps">
<a href="/docs/lint/rules/a11y/useAriaProps">useAriaProps</a>
<a class="header-anchor" href="#useAriaProps"></a>
@@ -532,13 +539,6 @@ discourage the usage of `onChange`
enforce explicit role property is not the same as implicit/default role property on element
</div>
<div class="rule">
-<h3 data-toc-exclude id="useAnchorContent">
- <a href="/docs/lint/rules/jsx-a11y/useAnchorContent">useAnchorContent</a>
- <a class="header-anchor" href="#useAnchorContent"></a>
-</h3>
-enforce that anchors have content and that the content is accessible to screen readers
-</div>
-<div class="rule">
<h3 data-toc-exclude id="useAriaPropsForRole">
<a href="/docs/lint/rules/jsx-a11y/useAriaPropsForRole">useAriaPropsForRole</a>
<a class="header-anchor" href="#useAriaPropsForRole"></a>

feat(lint): useAnchorContent for HTML (#1444)