feat: add react/jsx-no-target-blank lint rule
MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -105,6 +105,7 @@ type LintDiagnosticCategory =
| "lint/jsxA11yHTMLHasLang"
| "lint/jsxA11yIframeHasTitle"
| "lint/jsxA11yImgRedundantAlt"
+ | "lint/jsxA11yNoTargetBlank"
| "lint/jsxKey"
| "lint/jsxNoCommentText"
| "lint/negationElse"

MODIFY
packages/@romejs/diagnostics/descriptions.ts
packages/@romejs/diagnostics/descriptions.ts
@@ -425,6 +425,10 @@ export const descriptions = createMessages({
category: "lint/jsxNoCommentText",
message: "Comments inside children should be placed in braces",
},
+ REACT_JSX_NO_TARGET_BLANK: {
+ category: "lint/jsxA11yNoTargetBlank",
+ message: `Using <emphasis>target="_blank"</emphasis> without <emphasis>rel="noreferrer"</emphasis> is a security risk.`,
+ },
REACT_JSX_A11Y_IFRAME_HAS_TITLE: {
category: "lint/jsxA11yIframeHasTitle",
message: `<emphasis>iframe</emphasis> elements should have a <emphasis>title prop</emphasis>.`,

MODIFY
packages/@romejs/js-compiler/lint/rules/index.ts
packages/@romejs/js-compiler/lint/rules/index.ts
@@ -19,6 +19,7 @@ import inconsiderateLanguage from "./regular/inconsiderateLanguage";
import jsxA11YHTMLHasLang from "./react/jsxA11yHTMLHasLang";
import jsxA11YIframeHasTitle from "./react/jsxA11yIframeHasTitle";
import jsxA11YImgRedundantAlt from "./react/jsxA11yImgRedundantAlt";
+import jsxA11YNoTargetBlank from "./react/jsxA11yNoTargetBlank";
import jsxKey from "./react/jsxKey";
import jsxNoCommentText from "./react/jsxNoCommentText";
import negationElse from "./regular/negationElse";
@@ -83,6 +84,7 @@ export const lintTransforms = [
jsxA11YHTMLHasLang,
jsxA11YIframeHasTitle,
jsxA11YImgRedundantAlt,
+ jsxA11YNoTargetBlank,
jsxKey,
jsxNoCommentText,
negationElse,

ADD
packages/@romejs/js-compiler/lint/rules/react/jsxA11yNoTargetBlank.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ 'require anchors with target="_blank" to use rel="noreferrer"',
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID
+ '<a href="http://external.link" target="_blank">child</a>',
+ '<a href={dynamicLink} target="_blank">child</a>',
+ // VALID
+ '<p href="http://external.link" target="_blank">child</p>',
+ '<a href="http://external.link" rel="noreferrer" target="_blank">child</a>',
+ '<a href="http://external.link" rel="noopener noreferrer" target="_blank">child</a>',
+ '<a href="relative/link" rel="noreferrer" target="_blank">child</a>',
+ '<a href="/absolute/link" rel="noreferrer" target="_blank">child</a>',
+ ],
+ {category: "lint/jsxA11yNoTargetBlank"},
+ );
+ },
+);

ADD
packages/@romejs/js-compiler/lint/rules/react/jsxA11yNoTargetBlank.ts
+import {descriptions} from "@romejs/diagnostics";
+import {AnyNode} from "@romejs/js-ast";
+import {Path} from "@romejs/js-compiler";
+
+function jsxAnchorHasBlankTarget(node: AnyNode) {
+ return (
+ node.type === "JSXElement" &&
+ node.name.type === "JSXIdentifier" &&
+ node.name.name === "a" &&
+ node.attributes.some((attribute) =>
+ attribute.type === "JSXAttribute" &&
+ attribute.name.name === "target" &&
+ attribute.value &&
+ attribute.value.type === "StringLiteral" &&
+ attribute.value.value === "_blank"
+ )
+ );
+}
+
+function jsxAnchorHasNoReferrer(node: AnyNode) {
+ return (
+ node.type === "JSXElement" &&
+ node.name.type === "JSXIdentifier" &&
+ node.name.name === "a" &&
+ node.attributes.some((attribute) =>
+ attribute.type === "JSXAttribute" &&
+ attribute.name.name === "rel" &&
+ attribute.value &&
+ attribute.value.type === "StringLiteral" &&
+ attribute.value.value.includes("noreferrer")
+ )
+ );
+}
+
+function jsxAnchorHasExternalLink(node: AnyNode) {
+ return (
+ node.type === "JSXElement" &&
+ node.name.type === "JSXIdentifier" &&
+ node.name.name === "a" &&
+ node.attributes.some((attribute) =>
+ attribute.type === "JSXAttribute" &&
+ attribute.name.name === "href" &&
+ attribute.value &&
+ ((attribute.value.type === "StringLiteral" &&
+ /^(?:\w+:|\/\/)/.test(attribute.value.value)) ||
+ attribute.value.type === "JSXExpressionContainer")
+ )
+ );
+}
+
+export default {
+ name: "jsxA11yNoTargetBlank",
+
+ enter(path: Path): AnyNode {
+ const {node} = path;
+
+ if (
+ jsxAnchorHasBlankTarget(node) &&
+ !jsxAnchorHasNoReferrer(node) &&
+ jsxAnchorHasExternalLink(node)
+ ) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.REACT_JSX_NO_TARGET_BLANK,
+ );
+ }
+
+ return node;
+ },
+};

feat: add react/jsx-no-target-blank lint rule