feat(rome_js_analyzer): rule `noDangerouslySetInnerHtmlWithChildren`
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -51,6 +51,7 @@ define_dategories! {
"lint/nursery/noChildrenProp": "https://rome.tools/docs/lint/rules/noChildrenProp",
"lint/nursery/useFragmentSyntax": "https://rome.tools/docs/lint/rules/useFragmentSyntax",
"lint/nursery/noArrayIndexKey": "https://rome.tools/docs/lint/rules/noArrayIndexKey",
+ "lint/nursery/noDangerouslySetInnerHtmlWithChildren": "https://rome.tools/docs/lint/rules/noDangerouslySetInnerHtmlWithChildren",
"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/semantic_analyzers/nursery.rs
crates/rome_js_analyze/src/semantic_analyzers/nursery.rs
@@ -4,6 +4,7 @@ use rome_analyze::declare_group;
mod no_array_index_key;
mod no_children_prop;
mod no_dangerously_set_inner_html;
+mod no_dangerously_set_inner_html_with_children;
mod no_render_return_value;
mod no_undeclared_variables;
mod no_unused_variables;
@@ -12,4 +13,4 @@ mod no_void_elements_with_children;
mod use_button_type;
mod use_camel_case;
mod use_fragment_syntax;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_array_index_key :: NoArrayIndexKey , self :: no_children_prop :: NoChildrenProp , self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_render_return_value :: NoRenderReturnValue , self :: no_undeclared_variables :: NoUndeclaredVariables , self :: no_unused_variables :: NoUnusedVariables , self :: no_useless_fragments :: NoUselessFragments , self :: no_void_elements_with_children :: NoVoidElementsWithChildren , self :: use_button_type :: UseButtonType , self :: use_camel_case :: UseCamelCase , self :: use_fragment_syntax :: UseFragmentSyntax ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_array_index_key :: NoArrayIndexKey , self :: no_children_prop :: NoChildrenProp , self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_dangerously_set_inner_html_with_children :: NoDangerouslySetInnerHtmlWithChildren , self :: no_render_return_value :: NoRenderReturnValue , self :: no_undeclared_variables :: NoUndeclaredVariables , self :: no_unused_variables :: NoUnusedVariables , self :: no_useless_fragments :: NoUselessFragments , self :: no_void_elements_with_children :: NoVoidElementsWithChildren , self :: use_button_type :: UseButtonType , self :: use_camel_case :: UseCamelCase , self :: use_fragment_syntax :: UseFragmentSyntax ,] } }

ADD
crates/rome_js_analyze/src/semantic_analyzers/nursery/no_dangerously_set_inner_html_with_children.rs
+use crate::react::{ReactApiCall, ReactCreateElementCall};
+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::{
+ JsCallExpression, JsPropertyObjectMember, JsSyntaxNode, JsxAttribute, JsxElement,
+ JsxSelfClosingElement,
+};
+use rome_rowan::{declare_node_union, AstNode, AstNodeList, TextRange};
+
+declare_rule! {
+ /// Report when a DOM element or a component uses both `children` and `dangerouslySetInnerHTML` prop.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// function createMarkup() {
+ /// return { __html: 'child' }
+ /// }
+ /// <Component dangerouslySetInnerHTML={createMarkup()}>"child1"</Component>
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// function createMarkup() {
+ /// return { __html: 'child' }
+ /// }
+ /// <Component dangerouslySetInnerHTML={createMarkup()} children="child1" />
+ /// ```
+ ///
+ /// ```js,expect_diagnostic
+ /// React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } }, 'children')
+ /// ```
+ pub(crate) NoDangerouslySetInnerHtmlWithChildren {
+ version: "0.10.0",
+ name: "noDangerouslySetInnerHtmlWithChildren",
+ recommended: false,
+ }
+}
+
+declare_node_union! {
+ pub(crate) DangerousProp = JsxAttribute | JsPropertyObjectMember
+}
+/// The kind of children
+enum ChildrenKind {
+ /// As prop, e.g.
+ /// ```jsx
+ /// <Component children="child" />
+ /// ```
+ Prop(DangerousProp),
+ /// As direct descendent, e.g.
+ /// ```jsx
+ /// <ComponentA><ComponentB /> </ComponentA>
+ /// ```
+ Direct(JsSyntaxNode),
+}
+
+impl ChildrenKind {
+ fn text_trimmed_range(&self) -> TextRange {
+ match self {
+ ChildrenKind::Prop(prop) => prop.syntax().text_trimmed_range(),
+ ChildrenKind::Direct(node) => node.text_trimmed_range(),
+ }
+ }
+}
+
+pub(crate) struct RuleState {
+ /// The `dangerouslySetInnerHTML` prop
+ dangerous_prop: DangerousProp,
+
+ /// The kind of `children` found
+ children_kind: ChildrenKind,
+}
+
+declare_node_union! {
+ pub(crate) JsAnyCreateElement = JsxElement | JsxSelfClosingElement | JsCallExpression
+}
+
+impl JsAnyCreateElement {
+ /// If checks if the element has direct children (no children prop)
+ fn has_children(&self, model: &SemanticModel) -> Option<JsSyntaxNode> {
+ match self {
+ JsAnyCreateElement::JsxElement(element) => {
+ if !element.children().is_empty() {
+ Some(element.children().syntax().clone())
+ } else {
+ None
+ }
+ }
+ JsAnyCreateElement::JsxSelfClosingElement(_) => None,
+ JsAnyCreateElement::JsCallExpression(expression) => {
+ let react_create_element =
+ ReactCreateElementCall::from_call_expression(expression, model)?;
+
+ react_create_element
+ .children
+ .map(|children| children.syntax().clone())
+ }
+ }
+ }
+ fn find_dangerous_prop(&self, model: &SemanticModel) -> Option<DangerousProp> {
+ match self {
+ JsAnyCreateElement::JsxElement(element) => {
+ let opening_element = element.opening_element().ok()?;
+
+ opening_element
+ .find_attribute_by_name("dangerouslySetInnerHTML")
+ .ok()?
+ .map(DangerousProp::from)
+ }
+ JsAnyCreateElement::JsxSelfClosingElement(element) => element
+ .find_attribute_by_name("dangerouslySetInnerHTML")
+ .ok()?
+ .map(DangerousProp::from),
+ JsAnyCreateElement::JsCallExpression(call_expression) => {
+ let react_create_element =
+ ReactCreateElementCall::from_call_expression(call_expression, model)?;
+
+ react_create_element
+ .find_prop_by_name("dangerouslySetInnerHTML")
+ .map(DangerousProp::from)
+ }
+ }
+ }
+
+ fn find_children_prop(&self, model: &SemanticModel) -> Option<DangerousProp> {
+ match self {
+ JsAnyCreateElement::JsxElement(element) => {
+ let opening_element = element.opening_element().ok()?;
+
+ opening_element
+ .find_attribute_by_name("children")
+ .ok()?
+ .map(DangerousProp::from)
+ }
+ JsAnyCreateElement::JsxSelfClosingElement(element) => element
+ .find_attribute_by_name("children")
+ .ok()?
+ .map(DangerousProp::from),
+ JsAnyCreateElement::JsCallExpression(call_expression) => {
+ let react_create_element =
+ ReactCreateElementCall::from_call_expression(call_expression, model)?;
+
+ react_create_element
+ .find_prop_by_name("children")
+ .map(DangerousProp::from)
+ }
+ }
+ }
+}
+
+impl Rule for NoDangerouslySetInnerHtmlWithChildren {
+ const CATEGORY: RuleCategory = RuleCategory::Lint;
+ type Query = Semantic<JsAnyCreateElement>;
+ type State = RuleState;
+ type Signals = Option<Self::State>;
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let model = ctx.model();
+
+ if let Some(dangerous_prop) = node.find_dangerous_prop(model) {
+ if let Some(children_node) = node.has_children(model) {
+ return Some(RuleState {
+ children_kind: ChildrenKind::Direct(children_node),
+ dangerous_prop,
+ });
+ } else if let Some(children_prop) = node.find_children_prop(model) {
+ return Some(RuleState {
+ children_kind: ChildrenKind::Prop(children_prop),
+ dangerous_prop,
+ });
+ }
+ }
+
+ None
+ }
+
+ fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ state.dangerous_prop.syntax().text_trimmed_range(),
+ markup! {
+ "Avoid passing both "<Emphasis>"children"</Emphasis>" and the "<Emphasis>"dangerouslySetInnerHTML"</Emphasis>" prop."
+ },
+ ).secondary(state.children_kind.text_trimmed_range(), markup! {
+ "This is the source of the children prop"
+ }).footer_note(
+ markup! {
+ "Setting HTML content will inadvertently override any passed children in React"
+ }
+ ))
+ }
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noDangerouslySetInnerHtmlWithChildren/createElement.js
+React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } }, ['children'])
+React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } }, 'children')
+React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' }, children: 'children' })
\ No newline at end of file

ADD
crates/rome_js_analyze/tests/specs/nursery/noDangerouslySetInnerHtmlWithChildren/inJsx.jsx
+<>
+ <div dangerouslySetInnerHTML={{ __html: 'HTML' }}>children</div>
+ <div dangerouslySetInnerHTML={{ __html: 'HTML' }} children={'children'} />
+ <div dangerouslySetInnerHTML={{ __html: 'HTML' }} children={['children']} />
+ <Invalid dangerouslySetInnerHTML={{ __html: 'HTML' }}>children</Invalid>
+ <Invalid dangerouslySetInnerHTML={{ __html: 'HTML' }} children={'children'} />
+</>
\ No newline at end of file

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -388,6 +388,7 @@ pub struct Nursery {
struct NurserySchema {
no_array_index_key: Option<RuleConfiguration>,
no_dangerously_set_inner_html: Option<RuleConfiguration>,
+ no_dangerously_set_inner_html_with_children: Option<RuleConfiguration>,
no_new_symbol: Option<RuleConfiguration>,
no_render_return_value: Option<RuleConfiguration>,
no_undeclared_variables: Option<RuleConfiguration>,
@@ -402,9 +403,10 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 13] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 14] = [
"noArrayIndexKey",
"noDangerouslySetInnerHtml",
+ "noDangerouslySetInnerHtmlWithChildren",
"noNewSymbol",
"noRenderReturnValue",
"noUndeclaredVariables",

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

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -157,6 +157,7 @@ export interface Correctness {
export interface Nursery {
noArrayIndexKey?: RuleConfiguration;
noDangerouslySetInnerHtml?: RuleConfiguration;
+ noDangerouslySetInnerHtmlWithChildren?: RuleConfiguration;
noNewSymbol?: RuleConfiguration;
noRenderReturnValue?: RuleConfiguration;
noUndeclaredVariables?: RuleConfiguration;
@@ -306,6 +307,7 @@ export type Category =
| "lint/nursery/noChildrenProp"
| "lint/nursery/useFragmentSyntax"
| "lint/nursery/noArrayIndexKey"
+ | "lint/nursery/noDangerouslySetInnerHtmlWithChildren"
| "lint/style/noNegationElse"
| "lint/style/noShoutyConstants"
| "lint/style/useSelfClosingElements"

MODIFY
rome.json
rome.json
"useOptionalChain": "error",
"noUselessFragments": "error",
"noNewSymbol": "error",
- "noVoidElementsWithChildren": "error"
+ "noVoidElementsWithChildren": "error",
+ "noDangerouslySetInnerHtmlWithChildren": "error"
}
}
}

MODIFY
website/playground/src/romeWorker.ts
website/playground/src/romeWorker.ts
@@ -121,6 +121,7 @@ self.addEventListener("message", async (e) => {
useOptionalChain: "error",
noUselessFragments: "error",
noVoidElementsWithChildren: "error",
+ noDangerouslySetInnerHtmlWithChildren: "error",
},
},
};

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -267,6 +267,13 @@ Discourage the usage of Array index in keys.
Prevent the usage of dangerous JSX props
</div>
<div class="rule">
+<h3 data-toc-exclude id="noDangerouslySetInnerHtmlWithChildren">
+ <a href="/docs/lint/rules/noDangerouslySetInnerHtmlWithChildren">noDangerouslySetInnerHtmlWithChildren (since v0.10.0)</a>
+ <a class="header-anchor" href="#noDangerouslySetInnerHtmlWithChildren"></a>
+</h3>
+Report when a DOM element or a component uses both <code>children</code> and <code>dangerouslySetInnerHTML</code> prop.
+</div>
+<div class="rule">
<h3 data-toc-exclude id="noNewSymbol">
<a href="/docs/lint/rules/noNewSymbol">noNewSymbol (since v0.10.0)</a>
<a class="header-anchor" href="#noNewSymbol"></a>

feat(rome_js_analyzer): rule `noDangerouslySetInnerHtmlWithChildren` (#3314)