add AnchorIsValid rule and tests
MODIFY
packages/@romejs/compiler/lint/rules/index.ts
packages/@romejs/compiler/lint/rules/index.ts
@@ -57,6 +57,7 @@ import unsafeNegation from "./js/unsafeNegation";
import unusedVariables from "./js/unusedVariables";
import altText from "./jsx-a11y/altText";
import anchorHasContent from "./jsx-a11y/anchorHasContent";
+import anchorIsValid from "./jsx-a11y/anchorIsValid";
import ariaProps from "./jsx-a11y/ariaProps";
import ariaUnsupportedElements from "./jsx-a11y/ariaUnsupportedElements";
import clickEventsHaveKeyEvents from "./jsx-a11y/clickEventsHaveKeyEvents";
@@ -147,6 +148,7 @@ export const lintTransforms = [
unusedVariables,
altText,
anchorHasContent,
+ anchorIsValid,
ariaProps,
ariaUnsupportedElements,
clickEventsHaveKeyEvents,

ADD
packages/@romejs/compiler/lint/rules/jsx-a11y/anchorIsValid.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "jsx-a11y anchor is valid",
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID
+ "<a href='#' onClick={foo} />",
+ "<a href={'#'} onClick={foo} />",
+ "<a href={`#`} onClick={foo} />",
+ "<a onClick={foo} />",
+ "<a href='javascript:void(0)' onClick={foo} />",
+ "<a href={'javascript:void(0)'} onClick={foo} />",
+ "<a href={`javascript:void(0)`} onClick={foo} />",
+ "<a />",
+ "<a href={undefined} />",
+ "<a href={null} />",
+ "<a href='#' />",
+ "<a href={'#'} />",
+ "<a href={`#`} />",
+ "<a href='javascript:void(0)' />",
+ "<a href={'javascript:void(0)'} />",
+ "<a href={`javascript:void(0)`} />",
+ // VALID
+ "<a href='https://github.com' />",
+ "<a href='#section' />",
+ "<a href='foo' />",
+ "<a href='/foo/bar' />",
+ "<a href={someValidPath} />",
+ "<a href='https://github.com' onClick={foo} />",
+ "<a href='#section' onClick={foo} />",
+ "<a href='foo' onClick={foo} />",
+ "<a href='/foo/bar' onClick={foo} />",
+ "<a href={someValidPath} onClick={foo} />",
+ ],
+ {category: "lint/jsx-a11y/anchorIsValid"},
+ );
+ },
+);

ADD
packages/@romejs/compiler/lint/rules/jsx-a11y/anchorIsValid.ts
+import {Path, TransformExitResult} from "@romejs/compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {
+ getJSXAttribute,
+ hasJSXAttribute,
+ isJSXElement,
+} from "@romejs/js-ast-utils";
+import {JSXAttribute} from "@romejs/ast";
+
+function hrefValue(attr: JSXAttribute | undefined, value: string): boolean {
+ if (attr === undefined) {
+ return false;
+ }
+ return (
+ ((attr?.value)?.type === "JSStringLiteral" && attr.value.value === value) ||
+ ((attr?.value)?.type === "JSXExpressionContainer" &&
+ attr.value.expression.type === "JSStringLiteral" &&
+ attr.value.expression.value === value) ||
+ ((attr?.value)?.type === "JSXExpressionContainer" &&
+ attr.value.expression.type === "JSTemplateLiteral" &&
+ attr.value.expression.quasis.some((element) => element.raw === value))
+ );
+}
+
+function falsyHref(attr: JSXAttribute | undefined): boolean {
+ if (
+ attr === undefined ||
+ (attr.value?.type === "JSXExpressionContainer" &&
+ attr.value.expression.type === "JSNullLiteral")
+ ) {
+ return true;
+ }
+ return false;
+}
+
+export default {
+ name: "jsxA11YAnchorIsValid",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (isJSXElement(node, "a")) {
+ const attr = getJSXAttribute(node, "href");
+
+ if (falsyHref(attr) && !hasJSXAttribute(node, "onClick")) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_ANCHOR_IS_VALID(
+ "Provide a <emphasis>href</emphasis> attribute for the <emphasis>a</emphasis> element.",
+ ),
+ );
+ }
+
+ if (attr && !hasJSXAttribute(node, "onClick")) {
+ if (hrefValue(attr, "#") || hrefValue(attr, "javascript:void(0)")) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_ANCHOR_IS_VALID(
+ "Provide a valid <emphasis>href</emphasis> attribute for the <emphasis>a</emphasis> element.",
+ ),
+ );
+ }
+ }
+
+ if (
+ (hasJSXAttribute(node, "onClick") && !hasJSXAttribute(node, "href")) ||
+ (hasJSXAttribute(node, "href") && hrefValue(attr, "#")) ||
+ hrefValue(attr, "javascript:void(0)")
+ ) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_ANCHOR_IS_VALID(
+ "Use a <emphasis>button</emphasis> element instead of an <emphasis>a</emphasis> element.",
+ ),
+ );
+ }
+ }
+
+ return node;
+ },
+};

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -143,6 +143,7 @@ type LintDiagnosticCategory =
| "lint/js/unusedVariables"
| "lint/jsx-a11y/altText"
| "lint/jsx-a11y/anchorHasContent"
+ | "lint/jsx-a11y/anchorIsValid"
| "lint/jsx-a11y/ariaProps"
| "lint/jsx-a11y/ariaUnsupportedElements"
| "lint/jsx-a11y/clickEventsHaveKeyEvents"

MODIFY
packages/@romejs/diagnostics/descriptions/lint.ts
packages/@romejs/diagnostics/descriptions/lint.ts
@@ -16,6 +16,17 @@ import {buildSuggestionAdvice} from "../helpers";
import {createDiagnosticsCategory, orJoin} from "./index";
export const lint = createDiagnosticsCategory({
+ JSX_A11Y_ANCHOR_IS_VALID: (message: string) => ({
+ category: "lint/jsx-a11y/anchorIsValid",
+ message,
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: "Anchor elements should only be used for default section or page navigation.",
+ },
+ ],
+ }),
JSX_A11Y_ARIA_PROPS: (attribute: string) => ({
category: "lint/jsx-a11y/ariaProps",
message: `<emphasis>${attribute}</emphasis> is an invalid ARIA attribute.`,

add AnchorIsValid rule and tests (#524)