add rule jsxA11y alt text and tests
MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -102,6 +102,7 @@ type LintDiagnosticCategory =
| "lint/emptyMatches"
| "lint/getterReturn"
| "lint/importDefaultBasename"
+ | "lint/jsxA11yAltText"
| "lint/jsxA11yHTMLHasLang"
| "lint/jsxA11yIframeHasTitle"
| "lint/jsxA11yImgRedundantAlt"

MODIFY
packages/@romejs/diagnostics/descriptions.ts
packages/@romejs/diagnostics/descriptions.ts
@@ -290,6 +290,10 @@ export const descriptions = createMessages({
},
// @romejs/js-compiler
LINT: {
+ JSX_A11Y_ALT_TEXT: {
+ category: "lint/jsxA11yAltText",
+ message: "<emphasis>img</emphasis>, <emphasis>area</emphasis>, <emphasis>input type='image'</emphasis>, <emphasis>object</emphasis> must have alt text",
+ },
NO_DANGER: {
category: "lint/noDanger",
message: "dangerouslySetInnerHTML should be avoided",

MODIFY
packages/@romejs/js-compiler/lint/rules/index.ts
packages/@romejs/js-compiler/lint/rules/index.ts
@@ -16,6 +16,7 @@ import emptyBlocks from "./regular/emptyBlocks";
import emptyMatches from "./regular/emptyMatches";
import getterReturn from "./regular/getterReturn";
import importDefaultBasename from "./regular/importDefaultBasename";
+import jsxA11YAltText from "./react/jsxA11yAltText";
import jsxA11YHTMLHasLang from "./react/jsxA11yHTMLHasLang";
import jsxA11YIframeHasTitle from "./react/jsxA11yIframeHasTitle";
import jsxA11YImgRedundantAlt from "./react/jsxA11yImgRedundantAlt";
@@ -81,6 +82,7 @@ export const lintTransforms = [
emptyMatches,
getterReturn,
importDefaultBasename,
+ jsxA11YAltText,
jsxA11YHTMLHasLang,
jsxA11YIframeHasTitle,
jsxA11YImgRedundantAlt,

ADD
packages/@romejs/js-compiler/lint/rules/react/jsxA11yAltText.test.ts
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "require alt text in <img>, <area>, <input type='image'> and <object>",
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID,
+ "<img src='foo' />",
+ "<img {...props} />",
+ "<img {...props} alt />",
+ "<img {...props} alt={undefined} />",
+ "<img {...props} alt={`${undefined}`} />",
+ "<img src='foo' role='presentation' />",
+ "<img src='foo' role='none' />",
+ "<object {...props} />",
+ "<object aria-label />",
+ "<object aria-label={undefined} />",
+ "<object aria-label={`${undefined}`} />",
+ "<area {...props} />",
+ "<area {...props} alt />",
+ "<area alt={undefined} />",
+ "<area {...props} alt={`${undefined}`} />",
+ "<input type='image' {...props} />",
+ "<input type='image' {...props} alt />",
+ "<input type='image' {...props} alt={undefined} />",
+ "<input type='image' {...props} alt={`${undefined}`} />",
+
+ // VALID
+ "<img src='foo' alt='Foo eating a sandwich.' />",
+ "<img src='foo' alt={'Foo eating a sandwich.'} />",
+ "<img src='foo' alt={altText} />",
+ "<img src='foo' alt={`${person} smiling`} />",
+ "<img src='foo' alt='' />",
+ "<object aria-label='foo' />",
+ "<object aria-labelledby='id1' />",
+ "<object>Meaningful description</object>",
+ "<object>{hello}</object>",
+ "<object title='An object' />",
+ "<area aria-label='foo' />",
+ "<area aria-labelledby='id1' />",
+ "<area alt='This is descriptive!' />",
+ "<input type='image' alt='This is descriptive!' />",
+ "<input type='image' aria-label='foo' />",
+ "<input type='image' aria-labelledby='id1' />"
+ ],
+ {category: "lint/jsxA11yAltText"},
+ );
+ },
+);

ADD
packages/@romejs/js-compiler/lint/rules/react/jsxA11yAltText.ts
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {Path, TransformExitResult} from "@romejs/js-compiler";
+import {JSXElement, AnyNode} from "@romejs/js-ast";
+import {descriptions} from "@romejs/diagnostics";
+
+function hasUndefinedAltValue(node: AnyNode): boolean {
+ if (node.type !== "JSXExpressionContainer") {
+ return false;
+ }
+ return (
+ (node.expression.type === "ReferenceIdentifier" &&
+ node.expression.name === "undefined") ||
+ (node.expression.type === "TemplateLiteral" &&
+ node.expression.expressions.some(
+ expression =>
+ expression.type === "ReferenceIdentifier" &&
+ expression.name === "undefined"
+ ))
+ );
+}
+
+function hasImgAltText(node: JSXElement): boolean {
+ return node.attributes.some(
+ attr =>
+ attr.type === "JSXAttribute" &&
+ attr.name.name === "alt" &&
+ attr.value &&
+ !hasUndefinedAltValue(attr.value)
+ );
+}
+
+function hasObjectAltText(node: JSXElement): boolean {
+ return (
+ node.attributes.some(
+ attr =>
+ attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" &&
+ /(aria-label)|(aria-labelledby)|(title)/.test(attr.name.name) &&
+ attr.value &&
+ !hasUndefinedAltValue(attr.value)
+ ) || node.children.length > 0
+ );
+}
+
+function hasAreaAltText(node: JSXElement): boolean {
+ return node.attributes.some(
+ attr =>
+ attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" &&
+ /(aria-label)|(aria-labelledby)|(alt)|(title)/.test(attr.name.name) &&
+ attr.value &&
+ !hasUndefinedAltValue(attr.value)
+ );
+}
+
+function hasInputAltText(node: JSXElement): boolean {
+ return node.attributes.some(
+ attr =>
+ attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" &&
+ /(aria-label)|(aria-labelledby)|(alt)|(title)/.test(attr.name.name) &&
+ attr.value &&
+ !hasUndefinedAltValue(attr.value)
+ );
+}
+
+function hasTypeImage(node: JSXElement): boolean {
+ return node.attributes.some(
+ attr =>
+ attr.type === "JSXAttribute" &&
+ attr.name.name === "type" &&
+ attr.value &&
+ attr.value.type === "StringLiteral" &&
+ attr.value.value === "image"
+ );
+}
+
+export default {
+ name: "jsxA11yAltText",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (node.type === "JSXElement" && node.name.type === "JSXIdentifier") {
+ if (!/(img)|(area)|(input)|(object)/.test(node.name.name)) {
+ return node;
+ }
+
+ if (
+ (node.name.name === "img" && !hasImgAltText(node)) ||
+ (node.name.name === "object" && !hasObjectAltText(node)) ||
+ (node.name.name === "area" && !hasAreaAltText(node)) ||
+ (node.name.name === "input" &&
+ hasTypeImage(node) &&
+ !hasInputAltText(node))
+ ) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_ALT_TEXT
+ );
+ }
+ }
+
+ return node;
+ }
+};

add rule jsxA11y alt text and tests