feat(rome_js_analyzer): rule `useFragmentSyntax`
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -46,6 +46,7 @@ define_dategories! {
"lint/nursery/useButtonType": "https://rome.tools/docs/lint/rules/useButtonType",
"lint/nursery/useCamelCase": "https://rome.tools/docs/lint/rules/useCamelCase",
"lint/nursery/useOptionalChain": "https://rome.tools/docs/lint/rules/useOptionalChain",
+ "lint/nursery/useFragmentSyntax": "https://rome.tools/docs/lint/rules/useFragmentSyntax",
"lint/style/noNegationElse": "https://rome.tools/docs/lint/rules/noNegationElse",
"lint/style/noShoutyConstants": "https://rome.tools/docs/lint/rules/noShoutyConstants",
"lint/style/useSelfClosingElements": "https://rome.tools/docs/lint/rules/useSelfClosingElements",

MODIFY
crates/rome_js_analyze/src/react.rs
crates/rome_js_analyze/src/react.rs
+//! A series of utilities to work with the React library
+
use rome_js_semantic::SemanticModel;
use rome_js_syntax::{
JsAnyCallArgument, JsAnyExpression, JsArrayExpression, JsCallExpression, JsIdentifierBinding,
- JsImport, JsObjectExpression,
+ JsImport, JsObjectExpression, JsxMemberName, JsxReferenceIdentifier,
};
use rome_rowan::{AstNode, AstSeparatedList};
@@ -123,3 +125,64 @@ pub(crate) fn is_react_create_element(
None
}
}
+
+/// Checks if the node `JsxMemberName` is a react fragment.
+///
+/// e.g. `<React.Fragment>` is a fragment, but no `<React.StrictMode>`.
+///
+/// In case the `React` is a valid reference, the function checks if it is exported from the
+/// `"react"` library
+pub(crate) fn jsx_member_name_is_react_fragment(
+ member_name: &JsxMemberName,
+ model: &SemanticModel,
+) -> Option<bool> {
+ let object = member_name.object().ok()?;
+ let member = member_name.member().ok()?;
+ let object = object.as_jsx_reference_identifier()?;
+ let mut maybe_react_fragment = object.value_token().ok()?.text_trimmed() == "React"
+ && member.value_token().ok()?.text_trimmed() == "Fragment";
+ if let Some(reference) = model.declaration(object) {
+ if let Some(js_import) = reference
+ .syntax()
+ .ancestors()
+ .find_map(|ancestor| JsImport::cast_ref(&ancestor))
+ {
+ let source_is_react = js_import.source_is("react").ok()?;
+ maybe_react_fragment = source_is_react;
+ } else {
+ // `React.Fragment` is a binding but it doesn't come from the "react" package
+ maybe_react_fragment = false;
+ }
+ }
+
+ Some(maybe_react_fragment)
+}
+
+/// Checks if the node `JsxReferenceIdentifier` is a react fragment.
+///
+/// e.g. `<Fragment>` is a fragment
+///
+/// In case the `Fragment` is a valid reference, the function checks if it is exported from the
+/// `"react"` library
+pub(crate) fn jsx_reference_identifier_is_fragment(
+ name: &JsxReferenceIdentifier,
+ model: &SemanticModel,
+) -> Option<bool> {
+ let value_token = name.value_token().ok()?;
+ let mut maybe_react_fragment = value_token.text_trimmed() == "Fragment";
+ if let Some(reference) = model.declaration(name) {
+ if let Some(js_import) = reference
+ .syntax()
+ .ancestors()
+ .find_map(|ancestor| JsImport::cast_ref(&ancestor))
+ {
+ let source_is_react = js_import.source_is("react").ok()?;
+ maybe_react_fragment = source_is_react;
+ } else {
+ // `Fragment` is a binding g but it doesn't come from the "react" package
+ maybe_react_fragment = false;
+ }
+ }
+
+ Some(maybe_react_fragment)
+}

MODIFY
crates/rome_js_analyze/src/semantic_analyzers/nursery.rs
crates/rome_js_analyze/src/semantic_analyzers/nursery.rs
@@ -7,4 +7,5 @@ mod no_unused_variables;
mod no_useless_fragments;
mod use_button_type;
mod use_camel_case;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_render_return_value :: NoRenderReturnValue , self :: no_unused_variables :: NoUnusedVariables , self :: no_useless_fragments :: NoUselessFragments , self :: use_button_type :: UseButtonType , self :: use_camel_case :: UseCamelCase ,] } }
+mod use_fragment_syntax;
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_render_return_value :: NoRenderReturnValue , self :: no_unused_variables :: NoUnusedVariables , self :: no_useless_fragments :: NoUselessFragments , self :: use_button_type :: UseButtonType , self :: use_camel_case :: UseCamelCase , self :: use_fragment_syntax :: UseFragmentSyntax ,] } }

MODIFY
crates/rome_js_analyze/src/semantic_analyzers/nursery/no_useless_fragments.rs
crates/rome_js_analyze/src/semantic_analyzers/nursery/no_useless_fragments.rs
+use crate::react::{jsx_member_name_is_react_fragment, jsx_reference_identifier_is_fragment};
use crate::semantic_services::Semantic;
use rome_analyze::context::RuleContext;
use rome_analyze::{declare_rule, Rule, RuleCategory, RuleDiagnostic};
use rome_console::markup;
-use rome_js_semantic::SemanticModel;
use rome_js_syntax::{
- JsImport, JsSyntaxKind, JsxAnyChild, JsxAnyElementName, JsxElement, JsxFragment, JsxMemberName,
- JsxReferenceIdentifier, JsxTagExpression,
+ JsSyntaxKind, JsxAnyChild, JsxAnyElementName, JsxElement, JsxFragment, JsxTagExpression,
};
use rome_rowan::{declare_node_union, AstNode, AstNodeList};
@@ -131,52 +130,3 @@ impl Rule for NoUselessFragments {
))
}
}
-
-fn jsx_member_name_is_react_fragment(
- member_name: &JsxMemberName,
- model: &SemanticModel,
-) -> Option<bool> {
- let object = member_name.object().ok()?;
- let member = member_name.member().ok()?;
- let object = object.as_jsx_reference_identifier()?;
- let mut maybe_react_fragment = object.value_token().ok()?.text_trimmed() == "React"
- && member.value_token().ok()?.text_trimmed() == "Fragment";
- if let Some(reference) = model.declaration(object) {
- if let Some(js_import) = reference
- .syntax()
- .ancestors()
- .find_map(|ancestor| JsImport::cast_ref(&ancestor))
- {
- let source_is_react = js_import.source_is("react").ok()?;
- maybe_react_fragment = source_is_react;
- } else {
- // `React.Fragment` is a binding but it doesn't come from the "react" package
- maybe_react_fragment = false;
- }
- }
-
- Some(maybe_react_fragment)
-}
-
-fn jsx_reference_identifier_is_fragment(
- name: &JsxReferenceIdentifier,
- model: &SemanticModel,
-) -> Option<bool> {
- let value_token = name.value_token().ok()?;
- let mut maybe_react_fragment = value_token.text_trimmed() == "Fragment";
- if let Some(reference) = model.declaration(name) {
- if let Some(js_import) = reference
- .syntax()
- .ancestors()
- .find_map(|ancestor| JsImport::cast_ref(&ancestor))
- {
- let source_is_react = js_import.source_is("react").ok()?;
- maybe_react_fragment = source_is_react;
- } else {
- // `Fragment` is a binding g but it doesn't come from the "react" package
- maybe_react_fragment = false;
- }
- }
-
- Some(maybe_react_fragment)
-}

ADD
crates/rome_js_analyze/src/semantic_analyzers/nursery/use_fragment_syntax.rs
+use crate::react::{jsx_member_name_is_react_fragment, jsx_reference_identifier_is_fragment};
+use crate::semantic_services::Semantic;
+use crate::JsRuleAction;
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, ActionCategory, Rule, RuleCategory, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_factory::make::{
+ jsx_child_list, jsx_closing_fragment, jsx_fragment, jsx_opening_fragment,
+};
+use rome_js_syntax::{JsxAnyElementName, JsxElement};
+use rome_rowan::{AstNode, AstNodeList, BatchMutationExt};
+
+declare_rule! {
+ /// This rule enforces the use of `<>...</>` over `<Fragment>...</Fragment>`.
+ ///
+ /// The shorthand fragment syntax saves keystrokes and is only inapplicable when keys are required.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```js,expect_diagnostic
+ /// <Fragment>child</Fragment>
+ /// ```
+ ///
+ /// ```js,expect_diagnostic
+ /// <React.Fragment>child</React.Fragment>
+ /// ```
+ pub(crate) UseFragmentSyntax {
+ version: "0.10.0",
+ name: "useFragmentSyntax",
+ recommended: false,
+ }
+}
+
+impl Rule for UseFragmentSyntax {
+ const CATEGORY: RuleCategory = RuleCategory::Lint;
+ type Query = Semantic<JsxElement>;
+ type State = ();
+ type Signals = Option<Self::State>;
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let model = ctx.model();
+
+ let opening_element = node.opening_element().ok()?;
+ let name = opening_element.name().ok()?;
+ let maybe_invalid = match name {
+ JsxAnyElementName::JsxMemberName(member_name) => {
+ jsx_member_name_is_react_fragment(&member_name, model)?
+ }
+ JsxAnyElementName::JsxReferenceIdentifier(identifier) => {
+ jsx_reference_identifier_is_fragment(&identifier, model)?
+ }
+ JsxAnyElementName::JsxName(_) | JsxAnyElementName::JsxNamespaceName(_) => false,
+ };
+
+ if maybe_invalid && opening_element.attributes().is_empty() {
+ return Some(());
+ }
+
+ None
+ }
+
+ fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
+ let node = ctx.query();
+ let mut mutation = ctx.root().begin();
+ let list = jsx_child_list(node.children());
+ let opening_element = node.opening_element().ok()?;
+ let closing_element = node.closing_element().ok()?;
+ let fragment = jsx_fragment(
+ jsx_opening_fragment(
+ opening_element.l_angle_token().ok()?,
+ opening_element.r_angle_token().ok()?,
+ ),
+ list,
+ jsx_closing_fragment(
+ closing_element.l_angle_token().ok()?,
+ closing_element.slash_token().ok()?,
+ closing_element.r_angle_token().ok()?,
+ ),
+ );
+
+ mutation.replace_element(
+ node.clone().into_syntax().into(),
+ fragment.into_syntax().into(),
+ );
+
+ Some(JsRuleAction {
+ mutation,
+ message:
+ (markup! { "Replace "<Emphasis>"<Fragment>"</Emphasis>" with the fragment syntax" })
+ .to_owned(),
+ applicability: Applicability::MaybeIncorrect,
+ category: ActionCategory::QuickFix,
+ })
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
+ let node = ctx.query();
+
+ Some(
+ RuleDiagnostic::new(
+ node.syntax().text_trimmed_range(),
+ markup! {
+ "Use shorthand syntax for Fragment elements instead of standard syntax."
+ },
+ )
+ .footer_note(markup! {
+ "Shorthand fragment syntax saves keystrokes and is only inapplicable when keys are required."
+ }),
+ )
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/useFragmentSyntax.jsx
+import React, {Fragment} from "react";
+
+<div>
+ {/* invalid */}
+ <Fragment>
+ <p>Lorem</p>
+ <strong>Ipsum</strong>
+ </Fragment>
+
+ <React.Fragment>
+ <acronym>Lorem</acronym>
+ <aside>Ipsum</aside>
+ </React.Fragment>
+
+ </*comment*/Fragment>
+ <p>Lorem</p>
+ <strong>Ipsum</strong>
+ </ /*comment*/Fragment>
+
+ {/* valid */}
+ <Fragment key="1">
+ <p>Lorem</p>
+ <strong>Ipsum</strong>
+ </Fragment>
+
+ <React.Fragment key="1">
+ <acronym>Lorem</acronym>
+ <aside>Ipsum</aside>
+ </React.Fragment>
+</div>
\ No newline at end of file

MODIFY
crates/rome_js_syntax/src/jsx_ext.rs
crates/rome_js_syntax/src/jsx_ext.rs
-use crate::{JsxString, TextSize};
-use rome_rowan::{SyntaxResult, SyntaxTokenText};
+use crate::{
+ JsxAttribute, JsxAttributeList, JsxName, JsxOpeningElement, JsxSelfClosingElement, JsxString,
+ TextSize,
+};
+use rome_rowan::{AstNode, AstNodeList, SyntaxResult, SyntaxTokenText};
impl JsxString {
/// Get the inner text of a string not including the quotes
@@ -31,3 +34,122 @@ impl JsxString {
Ok(text)
}
}
+
+impl JsxOpeningElement {
+ /// Find and return the `JsxAttribute` that matches the given name
+ ///
+ /// ## Examples
+ ///
+ /// ```
+ ///
+ /// use rome_js_factory::make;
+ /// use rome_js_factory::make::{ident, jsx_attribute, jsx_name, jsx_opening_element, token, jsx_attribute_list};
+ /// use rome_js_syntax::{JsxAnyAttribute, JsxAnyAttributeName, JsxAnyElementName, T};
+ ///
+ /// let div = JsxAnyAttribute::JsxAttribute(jsx_attribute(
+ /// JsxAnyAttributeName::JsxName(
+ /// jsx_name(ident("div"))
+ /// )
+ /// ).build());
+ ///
+ /// let img = JsxAnyAttribute::JsxAttribute(jsx_attribute(
+ /// JsxAnyAttributeName::JsxName(
+ /// jsx_name(ident("img"))
+ /// )
+ /// ).build());
+ ///
+ /// let attributes = jsx_attribute_list(vec![
+ /// div,
+ /// img
+ /// ]);
+ ///
+ /// let opening_element = jsx_opening_element(
+ /// token(T![<]),
+ /// JsxAnyElementName::JsxName(
+ /// jsx_name(ident("Test"))
+ /// ),
+ /// attributes,
+ /// token(T![>]),
+ /// ).build();
+ ///
+ /// assert_eq!(opening_element.find_attribute_by_name("div").unwrap().is_some(), true);
+ /// assert_eq!(opening_element.find_attribute_by_name("img").unwrap().is_some(), true);
+ /// assert_eq!(opening_element.find_attribute_by_name("p").unwrap().is_some(), false);
+ /// ```
+ ///
+ pub fn find_attribute_by_name(
+ &self,
+ name_to_lookup: &str,
+ ) -> SyntaxResult<Option<JsxAttribute>> {
+ find_attribute_by_name(self.attributes(), name_to_lookup)
+ }
+}
+
+impl JsxSelfClosingElement {
+ /// Find and return the `JsxAttribute` that matches the given name
+ ///
+ /// ## Examples
+ ///
+ /// ```
+ ///
+ /// use rome_js_factory::make;
+ /// use rome_js_factory::make::{ident, jsx_attribute, jsx_name, jsx_opening_element, token, jsx_attribute_list, jsx_self_closing_element};
+ /// use rome_js_syntax::{JsxAnyAttribute, JsxAnyAttributeName, JsxAnyElementName, T};
+ ///
+ /// let div = JsxAnyAttribute::JsxAttribute(jsx_attribute(
+ /// JsxAnyAttributeName::JsxName(
+ /// jsx_name(ident("div"))
+ /// )
+ /// ).build());
+ ///
+ /// let img = JsxAnyAttribute::JsxAttribute(jsx_attribute(
+ /// JsxAnyAttributeName::JsxName(
+ /// jsx_name(ident("img"))
+ /// )
+ /// ).build());
+ ///
+ /// let attributes = jsx_attribute_list(vec![
+ /// div,
+ /// img
+ /// ]);
+ ///
+ /// let opening_element = jsx_self_closing_element(
+ /// token(T![<]),
+ /// JsxAnyElementName::JsxName(
+ /// jsx_name(ident("Test"))
+ /// ),
+ /// attributes,
+ /// token(T![/]),
+ /// token(T![>]),
+ /// ).build();
+ ///
+ /// assert_eq!(opening_element.find_attribute_by_name("div").unwrap().is_some(), true);
+ /// assert_eq!(opening_element.find_attribute_by_name("img").unwrap().is_some(), true);
+ /// assert_eq!(opening_element.find_attribute_by_name("p").unwrap().is_some(), false);
+ /// ```
+ ///
+ pub fn find_attribute_by_name(
+ &self,
+ name_to_lookup: &str,
+ ) -> SyntaxResult<Option<JsxAttribute>> {
+ find_attribute_by_name(self.attributes(), name_to_lookup)
+ }
+}
+
+pub fn find_attribute_by_name(
+ attributes: JsxAttributeList,
+ name_to_lookup: &str,
+) -> SyntaxResult<Option<JsxAttribute>> {
+ let attribute = attributes.iter().find_map(|attribute| {
+ let attribute = JsxAttribute::cast_ref(attribute.syntax())?;
+ let name = attribute.name().ok()?;
+ let name = JsxName::cast_ref(name.syntax())?;
+ if name.value_token().ok()?.text_trimmed() == name_to_lookup {
+ Some(attribute)
+ } else {
+ None
+ }
+ });
+
+ Ok(attribute)
+}

MODIFY
crates/rome_rowan/src/ast/batch.rs
crates/rome_rowan/src/ast/batch.rs
@@ -12,6 +12,7 @@ pub trait BatchMutationExt<L>: AstNode<Language = L>
where
L: Language,
{
+ /// It starts a [BatchMutation]
#[must_use = "This method consumes the node and return the BatchMutation api that returns the new SynytaxNode on commit"]
fn begin(self) -> BatchMutation<L>;
}

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -393,11 +393,12 @@ struct NurserySchema {
no_useless_fragments: Option<RuleConfiguration>,
use_button_type: Option<RuleConfiguration>,
use_camel_case: Option<RuleConfiguration>,
+ use_fragment_syntax: Option<RuleConfiguration>,
use_optional_chain: Option<RuleConfiguration>,
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 9] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 10] = [
"noDangerouslySetInnerHtml",
"noNewSymbol",
"noRenderReturnValue",
@@ -406,6 +407,7 @@ impl Nursery {
"noUselessFragments",
"useButtonType",
"useCamelCase",
+ "useFragmentSyntax",
"useOptionalChain",
];
const RECOMMENDED_RULES: [&'static str; 0] = [];

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

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -167,6 +167,7 @@ export interface Nursery {
recommended?: boolean;
useButtonType?: RuleConfiguration;
useCamelCase?: RuleConfiguration;
+ useFragmentSyntax?: RuleConfiguration;
useOptionalChain?: RuleConfiguration;
}
/**

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -309,6 +309,13 @@ Enforces the usage of the attribute <code>type</code> for the element <code>butt
Enforce camel case naming convention.
</div>
<div class="rule">
+<h3 data-toc-exclude id="useFragmentSyntax">
+ <a href="/docs/lint/rules/useFragmentSyntax">useFragmentSyntax (since v0.10.0)</a>
+ <a class="header-anchor" href="#useFragmentSyntax"></a>
+</h3>
+This rule enforces the use of <code>&lt;&gt;...&lt;/&gt;</code> over <code>&lt;Fragment&gt;...&lt;/Fragment&gt;</code>.
+</div>
+<div class="rule">
<h3 data-toc-exclude id="useOptionalChain">
<a href="/docs/lint/rules/useOptionalChain">useOptionalChain (since v0.10.0)</a>
<a class="header-anchor" href="#useOptionalChain"></a>

feat(rome_js_analyzer): rule `useFragmentSyntax` (#3279)