feat(rome_js_analyze): new rule `useBlankTarget`
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -53,6 +53,7 @@ define_dategories! {
"lint/nursery/noArrayIndexKey": "https://rome.tools/docs/lint/rules/noArrayIndexKey",
"lint/nursery/noDangerouslySetInnerHtmlWithChildren": "https://rome.tools/docs/lint/rules/noDangerouslySetInnerHtmlWithChildren",
"lint/nursery/noAutofocus": "https://rome.tools/docs/lint/rules/noAutofocus",
+ "lint/nursery/useBlankTarget": "https://rome.tools/docs/lint/rules/useBlankTarget",
"lint/nursery/useValidAnchor": "https://rome.tools/docs/lint/rules/useValidAnchor",
"lint/style/noNegationElse": "https://rome.tools/docs/lint/rules/noNegationElse",
"lint/style/noShoutyConstants": "https://rome.tools/docs/lint/rules/noShoutyConstants",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -4,6 +4,7 @@ use rome_analyze::declare_group;
mod no_auto_focus;
mod no_new_symbol;
mod no_unreachable;
+mod use_blank_target;
mod use_optional_chain;
mod use_valid_anchor;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_blank_target :: UseBlankTarget , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/use_blank_target.rs
+use crate::JsRuleAction;
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_factory::make::{
+ jsx_attribute, jsx_attribute_initializer_clause, jsx_attribute_list, jsx_ident, jsx_name,
+ jsx_string, token,
+};
+use rome_js_syntax::{
+ JsxAnyAttribute, JsxAnyAttributeName, JsxAnyAttributeValue, JsxAttribute, JsxAttributeList,
+ JsxElement, JsxSelfClosingElement, T,
+};
+use rome_rowan::{declare_node_union, AstNode, AstNodeList, BatchMutationExt, TriviaPieceKind};
+
+declare_rule! {
+ /// Disallow `target="_blank"` attribute without `rel="noreferrer"`
+ ///
+ /// When creating anchor `a` element, there are times when its link has to be opened in a new browser tab
+ /// via `target="_blank"` attribute. This attribute has to paired with `rel="noreferrer"` or you're incur
+ /// in a security issue.
+ ///
+ /// Refer to [the noreferrer documentation](https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer)
+ /// and the [the noopener documentation](https://html.spec.whatwg.org/multipage/links.html#link-type-noopener)
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// <a href='http://external.link' target='_blank'>child</a>
+ /// ```
+ /// ```jsx,expect_diagnostic
+ /// <a href='http://external.link' target='_blank' rel="noopener">child</a>
+ /// ```
+ /// ```jsx,expect_diagnostic
+ /// // case-insensitive
+ /// <a href='http://external.link' target='_BlaNk'>child</a>
+ /// ```
+ /// ### Valid
+ ///
+ /// ```jsx
+ /// <a href='http://external.link' rel='noreferrer' target='_blank'>child</a>
+ /// ```
+ pub(crate) UseBlankTarget {
+ version: "10.0.0",
+ name: "useBlankTarget",
+ recommended: false,
+ }
+}
+
+declare_node_union! {
+ pub(crate) UseBlankTargetQuery = JsxElement | JsxSelfClosingElement
+}
+
+impl Rule for UseBlankTarget {
+ type Query = Ast<UseBlankTargetQuery>;
+ /// Two attributes:
+ /// 1. The attribute `target=`
+ /// 2. The attribute `rel=`, if present
+ type State = (JsxAttribute, Option<JsxAttribute>);
+ type Signals = Option<Self::State>;
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let (target_attribute, rel_attribute) = match node {
+ UseBlankTargetQuery::JsxElement(element) => {
+ let opening_element = element.opening_element().ok()?;
+ if opening_element.name().ok()?.text() != "a"
+ || opening_element
+ .find_attribute_by_name("href")
+ .ok()
+ .is_none()
+ {
+ return None;
+ }
+
+ (
+ opening_element.find_attribute_by_name("target").ok()?,
+ opening_element.find_attribute_by_name("rel").ok()?,
+ )
+ }
+ UseBlankTargetQuery::JsxSelfClosingElement(element) => {
+ if element.name().ok()?.text() != "a"
+ || element.find_attribute_by_name("href").ok().is_none()
+ {
+ return None;
+ }
+ (
+ element.find_attribute_by_name("target").ok()?,
+ element.find_attribute_by_name("rel").ok()?,
+ )
+ }
+ };
+
+ let target_attribute = target_attribute?;
+ let text = target_attribute
+ .initializer()?
+ .value()
+ .ok()?
+ .as_jsx_string()?
+ .inner_string_text()
+ .ok()?;
+
+ if text.to_lowercase() == "_blank" {
+ match rel_attribute {
+ None => {
+ return Some((target_attribute, None));
+ }
+ Some(rel_attribute) => {
+ let rel_text = rel_attribute
+ .initializer()?
+ .value()
+ .ok()?
+ .as_jsx_string()?
+ .inner_string_text()
+ .ok()?;
+ if !rel_text.text().contains("noreferrer") {
+ return Some((target_attribute, Some(rel_attribute)));
+ }
+ }
+ }
+ }
+
+ None
+ }
+
+ fn action(
+ ctx: &RuleContext<Self>,
+ (target_attribute, rel_attribute): &Self::State,
+ ) -> Option<JsRuleAction> {
+ let mut mutation = ctx.root().begin();
+ let message = if let Some(rel_attribute) = rel_attribute {
+ let old_jsx_string = rel_attribute.initializer()?.value().ok()?;
+ let old_jsx_string = old_jsx_string.as_jsx_string()?;
+ let rel_text = old_jsx_string.inner_string_text().ok()?;
+ let new_text = format!("\"noreferrer {rel_text}\"");
+ let new_jsx_string = jsx_string(jsx_ident(&new_text));
+ mutation.replace_node(old_jsx_string.clone(), new_jsx_string);
+
+ (markup! {
+ "Add the "<Emphasis>"\"noreferrer\""</Emphasis>" to the existing attribute."
+ })
+ .to_owned()
+ } else {
+ let old_attribute_list = target_attribute
+ .syntax()
+ .ancestors()
+ .find_map(JsxAttributeList::cast)?;
+ let mut new_attribute_list: Vec<_> = old_attribute_list.iter().collect();
+ let new_attribute = jsx_attribute(JsxAnyAttributeName::JsxName(jsx_name(
+ jsx_ident("rel").with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]),
+ )))
+ .with_initializer(jsx_attribute_initializer_clause(
+ token(T![=]),
+ JsxAnyAttributeValue::JsxString(jsx_string(jsx_ident("\"noreferrer\""))),
+ ))
+ .build();
+
+ new_attribute_list.push(JsxAnyAttribute::JsxAttribute(new_attribute));
+
+ mutation.replace_node(old_attribute_list, jsx_attribute_list(new_attribute_list));
+
+ (markup! {
+ "Add the "<Emphasis>"rel=\"noreferrer\""</Emphasis>" attribute."
+ })
+ .to_owned()
+ };
+
+ Some(JsRuleAction {
+ mutation,
+ message,
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::Always,
+ })
+ }
+
+ fn diagnostic(
+ _ctx: &RuleContext<Self>,
+ (target_attribute, _): &Self::State,
+ ) -> Option<RuleDiagnostic> {
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ target_attribute.syntax().text_trimmed_range(),
+ markup! {
+ "Avoid using "<Emphasis>"target=\"_blank\""</Emphasis>" without "<Emphasis>"rel=\"noreferrer\""</Emphasis>"."
+ },
+ ).footer_note(
+ markup!{
+ "Opening external links in new tabs without rel=\"noreferrer\" is a security risk. See \
+ "<Hyperlink href="https://html.spec.whatwg.org/multipage/links.html#link-type-noopener">"the explanation"</Hyperlink>" for more details."
+ }
+ ))
+ }
+}

MODIFY
crates/rome_js_analyze/src/lib.rs
crates/rome_js_analyze/src/lib.rs
@@ -119,7 +119,7 @@ mod tests {
#[ignore]
#[test]
fn quick_test() {
- const SOURCE: &str = r#"document;
+ const SOURCE: &str = r#"<a href='http://external.link' target='_blank'>child</a>
"#;
let parsed = parse(SOURCE, FileId::zero(), SourceType::jsx());

ADD
crates/rome_js_analyze/tests/specs/nursery/useBlankTarget.jsx
+<>
+ {/* invalid */}
+ <a href='http://external.link' target='_blank'>child</a>
+ <a href='http://external.link' target='_BLank'>child</a>
+ <a href={dynamicLink} target='_blank'>child</a>
+ <a href="http://external.link" target="_blank">child</a>
+ <a href="http://external.link" target="_blank" rel="noopener">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={dynamicLink} rel='noreferrer' target='_blank'>child</a>
+ <a rel="noreferrer" target="_blank">child</a>
+ <a rel="noreferrer" target="_BLank">child</a>
+</>
\ No newline at end of file

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -398,6 +398,7 @@ struct NurserySchema {
no_unused_variables: Option<RuleConfiguration>,
no_useless_fragments: Option<RuleConfiguration>,
no_void_elements_with_children: Option<RuleConfiguration>,
+ use_blank_target: Option<RuleConfiguration>,
use_button_type: Option<RuleConfiguration>,
use_camel_case: Option<RuleConfiguration>,
use_fragment_syntax: Option<RuleConfiguration>,
@@ -406,7 +407,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 17] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 18] = [
"noArrayIndexKey",
"noAutofocus",
"noChildrenProp",
@@ -419,6 +420,7 @@ impl Nursery {
"noUnusedVariables",
"noUselessFragments",
"noVoidElementsWithChildren",
+ "useBlankTarget",
"useButtonType",
"useCamelCase",
"useFragmentSyntax",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
"null"
]
},
+ "useBlankTarget": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useButtonType": {
"anyOf": [
{

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -171,6 +171,7 @@ export interface Nursery {
* It enables the recommended rules for this group
*/
recommended?: boolean;
+ useBlankTarget?: RuleConfiguration;
useButtonType?: RuleConfiguration;
useCamelCase?: RuleConfiguration;
useFragmentSyntax?: RuleConfiguration;
@@ -309,6 +310,7 @@ export type Category =
| "lint/nursery/noArrayIndexKey"
| "lint/nursery/noDangerouslySetInnerHtmlWithChildren"
| "lint/nursery/noAutofocus"
+ | "lint/nursery/useBlankTarget"
| "lint/nursery/useValidAnchor"
| "lint/style/noNegationElse"
| "lint/style/noShoutyConstants"

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -337,6 +337,13 @@ Disallow unnecessary fragments
This rules prevents void elements (AKA self-closing elements) from having children.
</div>
<div class="rule">
+<h3 data-toc-exclude id="useBlankTarget">
+ <a href="/docs/lint/rules/useBlankTarget">useBlankTarget (since v10.0.0)</a>
+ <a class="header-anchor" href="#useBlankTarget"></a>
+</h3>
+Disallow <code>target=&quot;_blank&quot;</code> attribute without <code>rel=&quot;noreferrer&quot;</code>
+</div>
+<div class="rule">
<h3 data-toc-exclude id="useButtonType">
<a href="/docs/lint/rules/useButtonType">useButtonType (since v0.10.0)</a>
<a class="header-anchor" href="#useButtonType"></a>

feat(rome_js_analyze): new rule `useBlankTarget` (#3356)