From 85e8418a78b2c61d9e7ce6cc7d62c61776358fe1 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Tue, 30 Jul 2024 22:30:55 -0400 Subject: [PATCH] feat(linter): add react/jsx-curly-brace-presence (#3949) Note that this PR does not implement a fixer, but one is available. --- crates/oxc_ast/src/ast/jsx.rs | 5 + crates/oxc_linter/src/rules.rs | 2 + .../rules/react/jsx_curly_brace_presence.rs | 957 ++++++++++++++++++ .../snapshots/jsx_curly_brace_presence.snap | 403 ++++++++ 4 files changed, 1367 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/jsx_curly_brace_presence.rs create mode 100644 crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap diff --git a/crates/oxc_ast/src/ast/jsx.rs b/crates/oxc_ast/src/ast/jsx.rs index e59c63929fe4e..a94e98bf5177f 100644 --- a/crates/oxc_ast/src/ast/jsx.rs +++ b/crates/oxc_ast/src/ast/jsx.rs @@ -370,6 +370,11 @@ pub enum JSXChild<'a> { /// `{...spread}` Spread(Box<'a, JSXSpreadChild<'a>>), } +impl<'a> JSXChild<'a> { + pub const fn is_expression_container(&self) -> bool { + matches!(self, Self::ExpressionContainer(_)) + } +} /// JSX Spread Child. /// diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index c0c8080ab0e5d..270723ae31125 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -217,6 +217,7 @@ mod jest { mod react { pub mod button_has_type; pub mod checked_requires_onchange_or_readonly; + pub mod jsx_curly_brace_presence; pub mod jsx_key; pub mod jsx_no_comment_textnodes; pub mod jsx_no_duplicate_props; @@ -716,6 +717,7 @@ oxc_macros::declare_all_lint_rules! { react::button_has_type, react::checked_requires_onchange_or_readonly, react::jsx_no_target_blank, + react::jsx_curly_brace_presence, react::jsx_key, react::jsx_no_comment_textnodes, react::jsx_no_duplicate_props, diff --git a/crates/oxc_linter/src/rules/react/jsx_curly_brace_presence.rs b/crates/oxc_linter/src/rules/react/jsx_curly_brace_presence.rs new file mode 100644 index 0000000000000..4712eff9aa48b --- /dev/null +++ b/crates/oxc_linter/src/rules/react/jsx_curly_brace_presence.rs @@ -0,0 +1,957 @@ +use oxc_allocator::Vec; +use oxc_ast::{ + ast::{ + Expression, JSXAttributeItem, JSXAttributeValue, JSXChild, JSXElementName, + JSXExpressionContainer, + }, + AstKind, +}; +use oxc_diagnostics::{Error, LabeledSpan, OxcDiagnostic}; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::AstNodeId; +use oxc_span::{GetSpan as _, Span}; +use serde_json::Value; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn jsx_curly_brace_presence_unnecessary_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Curly braces are unnecessary here.").with_label(span) +} +fn jsx_curly_brace_presence_necessary_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Curly braces are required here.") + .with_help("Wrap this value in curly braces") + .with_labels([LabeledSpan::new_primary_with_span( + Some("Wrap this value in curly braces".into()), + span, + )]) +} + +#[derive(Debug, Default, Clone, Copy)] +enum Allowed { + Always, + Never, + #[default] + Ignore, +} + +impl TryFrom<&str> for Allowed { + type Error = (); + fn try_from(value: &str) -> Result { + match value { + "always" => Ok(Self::Always), + "never" => Ok(Self::Never), + "ignore" => Ok(Self::Ignore), + _ => Err(()), + } + } +} + +impl Allowed { + pub fn is_never(self) -> bool { + matches!(self, Self::Never) + } + #[inline] + pub fn is_always(self) -> bool { + matches!(self, Self::Always) + } +} + +#[derive(Debug, Clone)] +pub struct JsxCurlyBracePresence { + props: Allowed, + children: Allowed, + prop_element_values: Allowed, +} + +impl Default for JsxCurlyBracePresence { + fn default() -> Self { + Self { + props: Allowed::Never, + children: Allowed::Never, + prop_element_values: Allowed::Ignore, + } + } +} + +declare_oxc_lint!( + /// # Disallow unnecessary JSX expressions when literals alone are + /// sufficient or enforce JSX expressions on literals in JSX children or + /// attributes (`react/jsx-curly-brace-presence`) + /// + /// ๐Ÿ”ง This rule is automatically fixable by the [`--fix` CLI option](https://oxc-project.github.io/docs/guide/usage/linter/cli.html#fix-problems). + /// + /// This rule allows you to enforce curly braces or disallow unnecessary + /// curly braces in JSX props and/or children. + /// + /// For situations where JSX expressions are unnecessary, please refer to + /// [the React doc](https://facebook.github.io/react/docs/jsx-in-depth.html) + /// and [this page about JSX + /// gotchas](https://github.com/facebook/react/blob/v15.4.0-rc.3/docs/docs/02.3-jsx-gotchas.md#html-entities). + /// + /// ## Rule Details + /// + /// By default, this rule will check for and warn about unnecessary curly + /// braces in both JSX props and children. For the sake of backwards + /// compatibility, prop values that are JSX elements are not considered by + /// default. + /// + /// You can pass in options to enforce the presence of curly braces on JSX + /// props, children, JSX prop values that are JSX elements, or any + /// combination of the three. The same options are available for not + /// allowing unnecessary curly braces as well as ignoring the check. + /// + /// **Note**: it is _highly recommended_ that you configure this rule with + /// an object, and that you set "propElementValues" to "always". The ability + /// to omit curly braces around prop values that are JSX elements is + /// obscure, and intentionally undocumented, and should not be relied upon. + /// + /// ## Rule Options + /// + /// ```js + /// ... + /// "react/jsx-curly-brace-presence": [, { "props": , "children": , "propElementValues": }] + /// ... + /// ``` + /// + /// or alternatively + /// + /// ```js + /// ... + /// "react/jsx-curly-brace-presence": [, ] + /// ... + /// ``` + /// + /// ### Valid options for `` + /// + /// They are `always`, `never` and `ignore` for checking on JSX props and + /// children. + /// + /// - `always`: always enforce curly braces inside JSX props, children, and/or JSX prop values that are JSX Elements + /// - `never`: never allow unnecessary curly braces inside JSX props, children, and/or JSX prop values that are JSX Elements + /// - `ignore`: ignore the rule for JSX props, children, and/or JSX prop values that are JSX Elements + /// + /// If passed in the option to fix, this is how a style violation will get fixed + /// + /// - `always`: wrap a JSX attribute in curly braces/JSX expression and/or a JSX child the same way but also with double quotes + /// - `never`: get rid of curly braces from a JSX attribute and/or a JSX child + /// + /// - All fixing operations use double quotes. + /// + /// For examples: + /// + /// Examples of **incorrect** code for this rule, when configured with `{ props: "always", children: "always" }`: + /// + /// ```jsx + /// Hello world; + /// {'Hello world'}; + /// ``` + /// + /// They can be fixed to: + /// + /// ```jsx + /// {"Hello world"}; + /// {'Hello world'}; + /// ``` + /// + /// Examples of **incorrect** code for this rule, when configured with `{ props: "never", children: "never" }`: + /// + /// ```jsx + /// {'Hello world'}; + /// ; + /// ``` + /// + /// They can be fixed to: + /// + /// ```jsx + /// Hello world; + /// ; + /// ``` + /// + /// Examples of **incorrect** code for this rule, when configured with `{ props: "always", children: "always", "propElementValues": "always" }`: + /// + /// ```jsx + /// />; + /// ``` + /// + /// They can be fixed to: + /// + /// ```jsx + /// } />; + /// ``` + /// + /// Examples of **incorrect** code for this rule, when configured with `{ props: "never", children: "never", "propElementValues": "never" }`: + /// + /// ```jsx + /// } />; + /// ``` + /// + /// They can be fixed to: + /// + /// ```jsx + /// />; + /// ``` + /// + /// ### Alternative syntax + /// + /// The options are also `always`, `never`, and `ignore` for the same meanings. + /// + /// In this syntax, only a string is provided and the default will be set to + /// that option for checking on both JSX props and children. + /// + /// For examples: + /// + /// Examples of **incorrect** code for this rule, when configured with `"always"`: + /// + /// ```jsx + /// Hello world; + /// Hello world; + /// ``` + /// + /// They can be fixed to: + /// + /// ```jsx + /// {"Hello world"}; + /// {"Hello world"}; + /// ``` + /// + /// Examples of **incorrect** code for this rule, when configured with `"never"`: + /// + /// ```jsx + /// {'Hello world'}; + /// ``` + /// + /// It can fixed to: + /// + /// ```jsx + /// Hello world; + /// ``` + /// + /// ## Edge cases + /// + /// The fix also deals with template literals, strings with quotes, and + /// strings with escapes characters. + /// + /// - If the rule is set to get rid of unnecessary curly braces and the + /// template literal inside a JSX expression has no expression, it will + /// throw a warning and be fixed with double quotes. For example: + /// + /// ```jsx + /// {`Hello world`}; + /// ``` + /// + /// will be warned and fixed to: + /// + /// ```jsx + /// Hello world; + /// ``` + /// + /// - If the rule is set to enforce curly braces and the strings have + /// quotes, it will be fixed with double quotes for JSX children and the + /// normal way for JSX attributes. Also, double quotes will be escaped in + /// the fix. + /// + /// For example: + /// + /// ```jsx + /// Hello 'foo' "bar" world; + /// ``` + /// + /// will warned and fixed to: + /// + /// ```jsx + /// {"Hello 'foo' \"bar\" world"}; + /// ``` + /// + /// - If the rule is set to get rid of unnecessary curly braces(JSX + /// expression) and there are characters that need to be escaped in its JSX + /// form, such as quote characters, [forbidden JSX text + /// characters](https://facebook.github.io/jsx/), escaped characters and + /// anything that looks like HTML entity names, the code will not be warned + /// because the fix may make the code less readable. + /// + /// Examples of **correct** code for this rule, even when configured with `"never"`: + /// + /// ```jsx + /// + /// {"Hello \u00b7 world"}; + /// ; + /// /** + /// * there's no way to inject a whitespace into jsx without a container so this + /// * will always be allowed. + /// */ + /// {' '} + /// {' '} + /// {/* comment */ } // the comment makes the container necessary + /// ``` + /// + /// ## When Not To Use It + /// + /// You should turn this rule off if you are not concerned about maintaining + /// consistency regarding the use of curly braces in JSX props and/or + /// children as well as the use of unnecessary JSX expressions. + JsxCurlyBracePresence, + style, +); + +impl Rule for JsxCurlyBracePresence { + fn from_configuration(value: Value) -> Self { + let default = Self::default(); + let value = if let Some(arr) = value.as_array() { &arr[0] } else { &value }; + match value { + Value::String(s) => { + let allowed = Allowed::try_from(s.as_str()) + .map_err(|()| Error::msg( + r#"Invalid string config for eslint-plugin-react/jsx-curly-brace-presence: only "always", "never", or "ignored" are allowed. "# + )).unwrap(); + Self { props: allowed, children: allowed, prop_element_values: allowed } + } + Value::Object(obj) => { + let props = obj + .get("props") + .and_then(Value::as_str) + .and_then(|props| Allowed::try_from(props).ok()) + .unwrap_or(default.props); + let children = obj + .get("children") + .and_then(Value::as_str) + .and_then(|children| Allowed::try_from(children).ok()) + .unwrap_or(default.children); + let prop_element_values = obj + .get("propElementValues") + .and_then(Value::as_str) + .and_then(|prop_element_values| Allowed::try_from(prop_element_values).ok()) + .unwrap_or(default.prop_element_values); + + Self { props, children, prop_element_values } + } + _ => default, + } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::JSXElement(el) => { + el.opening_element.attributes.iter().for_each(|attr| { + self.check_jsx_attribute(ctx, attr, node); + }); + if self.children.is_never() + && matches!(&el.opening_element.name, JSXElementName::Identifier(ident) if ident.name == "script") + { + return; + } + self.check_jsx_child(ctx, &el.children, node); + } + AstKind::JSXFragment(fragment) => { + self.check_jsx_child(ctx, &fragment.children, node); + } + _ => {} + } + } + + fn should_run(&self, ctx: &LintContext) -> bool { + ctx.source_type().is_jsx() + } +} + +impl JsxCurlyBracePresence { + fn check_jsx_child<'a>( + &self, + ctx: &LintContext<'a>, + children: &Vec<'a, JSXChild<'a>>, + node: &AstNode<'a>, + ) { + for child in children { + match child { + JSXChild::ExpressionContainer(container) => { + self.check_expression_container(ctx, container, node, false); + } + JSXChild::Text(text) => { + if self.children.is_always() + && children.len() == 1 + && !is_whitespace(&text.value) + { + ctx.diagnostic(jsx_curly_brace_presence_necessary_diagnostic(text.span)); + } + } + _ => {} + } + } + } + + fn check_jsx_attribute<'a>( + &self, + ctx: &LintContext<'a>, + attr: &JSXAttributeItem<'a>, + node: &AstNode<'a>, + ) { + let JSXAttributeItem::Attribute(attr) = attr else { + return; + }; + let Some(value) = attr.value.as_ref() else { return }; + + match value { + JSXAttributeValue::ExpressionContainer(container) => { + self.check_expression_container(ctx, container, node, true); + } + JSXAttributeValue::Element(el) => { + if self.prop_element_values.is_always() { + ctx.diagnostic(jsx_curly_brace_presence_necessary_diagnostic(el.span)); + } + } + JSXAttributeValue::Fragment(fragment) => { + if self.prop_element_values.is_always() { + ctx.diagnostic(jsx_curly_brace_presence_necessary_diagnostic(fragment.span)); + } + } + JSXAttributeValue::StringLiteral(string) => { + if self.props.is_always() { + ctx.diagnostic(jsx_curly_brace_presence_necessary_diagnostic(string.span)); + } + } + } + } + + fn check_expression_container<'a>( + &self, + ctx: &LintContext<'a>, + container: &JSXExpressionContainer<'a>, + node: &AstNode<'a>, + // true for JSX props, false for JSX children + is_prop: bool, + ) { + let Some(inner) = container.expression.as_expression() else { return }; + let allowed = if is_prop { self.props } else { self.children }; + match inner { + Expression::JSXFragment(_) => { + if !is_prop + && self.children.is_never() + && !has_adjacent_jsx_expression_containers(ctx, container, node.id()) + { + report_unnecessary_curly(ctx, container, inner.span()); + } + } + Expression::JSXElement(el) => { + if is_prop { + if self.prop_element_values.is_never() && el.closing_element.is_none() { + report_unnecessary_curly(ctx, container, inner.span()); + } + } else if self.children.is_never() + && !has_adjacent_jsx_expression_containers(ctx, container, node.id()) + { + report_unnecessary_curly(ctx, container, inner.span()); + } + } + Expression::StringLiteral(string) => { + if allowed.is_never() { + let raw = ctx.source_range(string.span().shrink_left(1).shrink_right(1)); + if is_allowed_string_like(ctx, raw, container, node.id(), is_prop) { + return; + } + report_unnecessary_curly(ctx, container, string.span); + } + } + Expression::TemplateLiteral(template) => { + if allowed.is_never() && template.is_no_substitution_template() { + let string = template.quasi().unwrap(); + if is_allowed_string_like(ctx, string.as_str(), container, node.id(), is_prop) { + return; + } + report_unnecessary_curly(ctx, container, template.span); + } + } + _ => {} + } + } +} + +fn is_allowed_string_like<'a>( + ctx: &LintContext<'a>, + s: &'a str, + container: &JSXExpressionContainer<'a>, + node_id: AstNodeId, + is_prop: bool, +) -> bool { + is_whitespace(s) + || is_line_break(s) + || contains_html_entity(s) + || !is_prop && contains_disallowed_jsx_text_chars(s) + || s.trim() != s + || contains_multiline_comment(s) + || contains_line_break_literal(s) + || contains_utf8_escape(s) + || is_prop && contains_quote_characters(s) + || has_adjacent_jsx_expression_containers(ctx, container, node_id) +} + +fn is_whitespace(s: &str) -> bool { + s.chars().all(char::is_whitespace) +} + +fn is_line_break(s: &str) -> bool { + s.chars().any(|c| matches!(c, '\n' | '\r')) || s.trim().is_empty() +} + +fn contains_line_break_literal(s: &str) -> bool { + s.chars().zip(s.chars().skip(1)).any(|tuple| matches!(tuple, ('\\', 'n' | 'r'))) +} + +fn contains_disallowed_jsx_text_chars(s: &str) -> bool { + s.chars().any(|c| matches!(c, '<' | '>' | '{' | '}' | '\\')) +} + +fn contains_multiline_comment(s: &str) -> bool { + s.contains("/*") || s.contains("*/") +} + +fn contains_quote_characters(s: &str) -> bool { + s.chars().any(|c| matches!(c, '"' | '\'')) +} + +fn contains_utf8_escape(s: &str) -> bool { + s.chars().zip(s.chars().skip(1)).any(|tuple| matches!(tuple, ('\\', 'u'))) +} + +fn contains_html_entity(s: &str) -> bool { + let and = s.find('&'); + let semi = s.find(';'); + matches!((and, semi), (Some(and), Some(semi)) if and < semi) +} + +fn report_unnecessary_curly<'a>( + ctx: &LintContext<'a>, + _container: &JSXExpressionContainer<'a>, + inner_span: Span, +) { + ctx.diagnostic(jsx_curly_brace_presence_unnecessary_diagnostic(inner_span)); +} + +fn has_adjacent_jsx_expression_containers<'a>( + ctx: &LintContext<'a>, + container: &JSXExpressionContainer<'a>, + node_id: AstNodeId, + // element: &JSXElement<'a>, +) -> bool { + let Some(parent) = ctx.semantic().nodes().parent_kind(node_id) else { return false }; + let children = match parent { + AstKind::JSXElement(el) => &el.children, + AstKind::JSXFragment(fragment) => &fragment.children, + AstKind::ExpressionStatement(expr) => match &expr.expression { + Expression::JSXElement(el) => &el.children, + Expression::JSXFragment(fragment) => &fragment.children, + _ => { + return false; + } + }, + _ => { + return false; + } + }; + let Some(this_container_idx) = children.iter().position(|child| child.span() == container.span) + else { + return false; + }; + + [this_container_idx.checked_sub(1), this_container_idx.checked_add(1)] + .into_iter() + // [prev id, next id] -> [prev node, next node], removing out-of-bounds indices + .filter_map(|idx| idx.and_then(|idx| children.get(idx))) + .any(oxc_ast::ast::JSXChild::is_expression_container) +} + +#[test] +fn test() { + use crate::tester::Tester; + use serde_json::json; + + let pass = vec![ + ("foo", None), + ("<>foo", None), + ("foo", Some(json!([{ "props": "never" }]))), + ("{' '}", None), + ( + "{' '} + ", + None, + ), + ("{' '}", None), + ( + "{' '} + ", + None, + ), + ("{' '}", Some(json!([{ "children": "never" }]))), + ("{' '}", Some(json!([{ "children": "never" }]))), + ("{' '}", Some(json!([{ "children": "always" }]))), + ("{' '}", Some(json!([{ "children": "always" }]))), + ("foo", Some(json!([{ "props": "always" }]))), + ("{`Hello ${word} World`}", Some(json!([{ "children": "never" }]))), + ( + " + + foo{' '} + bar + + ", + Some(json!([{ "children": "never" }])), + ), + ( + " + <> + foo{' '} + bar + + ", + Some(json!([{ "children": "never" }])), + ), + ("{`Hello \n World`}", Some(json!([{ "children": "never" }]))), + ("{`Hello ${word} World`}{`foo`}", Some(json!([{ "children": "never" }]))), + ("foo", Some(json!([{ "props": "never" }]))), + ("", Some(json!([{ "props": "never" }]))), + ("{}", Some(json!([{ "children": "always" }]))), + ("{[]}", None), + ("foo", None), + (r#"{"foo"}{bar}"#, None), + ("foo", None), + ("foo", None), + ("foo", None), + (r"{'foo \n bar'}", None), + ("", None), + ("foo", Some(json!([{ "props": "never" }]))), + (r#"foo"#, Some(json!([{ "props": "never" }]))), + ("foo", Some(json!([{ "children": "never" }]))), + (r#"{}{"123"}"#, Some(json!([{ "children": "never" }]))), + (r#"{"foo 'bar' \"foo\" bar"}"#, Some(json!([{ "children": "never" }]))), + ("foo", Some(json!([{ "props": "always" }]))), + ("{'foo'}", Some(json!([{ "children": "always" }]))), + (r#"foo"#, Some(json!([{ "props": "always" }]))), + (r#"{"foo"}"#, Some(json!([{ "children": "always" }]))), + ("{'foo'}", Some(json!([{ "children": "ignore" }]))), + ("foo", Some(json!([{ "props": "ignore" }]))), + ("foo", Some(json!([{ "children": "ignore" }]))), + ("foo", Some(json!([{ "props": "ignore" }]))), + (r#"foo"#, Some(json!([{ "props": "ignore" }]))), + ( + "{'foo'}", + Some(json!([{ "children": "always", "props": "never" }])), + ), + ( + "foo", + Some(json!([{ "children": "never", "props": "always" }])), + ), + ("{'foo'}", Some(json!(["always"]))), + (r#"{"foo"}"#, Some(json!(["always"]))), + (r#""#, Some(json!(["always"]))), + (r#""#, Some(json!(["never"]))), + ("foo", Some(json!(["never"]))), + ( + "{`foo ${word}`}", + Some(json!(["never"])), + ), + (r#"{"div { margin-top: 0; }"}"#, Some(json!(["never"]))), + (r#"{""}"#, Some(json!(["never"]))), + (r#"bar"#, Some(json!(["never"]))), + (r#"{"Hello \u1026 world"}"#, Some(json!(["never"]))), + (r#"bar"#, Some(json!(["never"]))), + (r#"{"Hello · world"}"#, Some(json!(["never"]))), + (r#"{"Hello \n world"}"#, Some(json!(["never"]))), + (r#"{"space after "}"#, Some(json!(["never"]))), + (r#"{" space before"}"#, Some(json!(["never"]))), + ("{`space after `}", Some(json!(["never"]))), + ("{` space before`}", Some(json!(["never"]))), + ( + " + + ", + Some(json!(["never"])), + ), + ( + " + + ", + Some(json!(["always"])), + ), + ( + " + + {` + a + b + `} + + ", + Some(json!(["never"])), + ), + ( + " + {` + a + b + `} + ", + Some(json!(["always"])), + ), + ( + " + + % + + ", + Some(json!([{ "children": "never" }])), + ), + ( + " + + { 'space after ' } + foo + { ' space before' } + + ", + Some(json!([{ "children": "never" }])), + ), + ( + " + + { `space after ` } + foo + { ` space before` } + + ", + Some(json!([{ "children": "never" }])), + ), + ( + " + + foo +
bar
+
+ ", + Some(json!([{ "children": "never" }])), + ), + ( + " + Bar}> + + ", + None, + ), + ( + r#" + +
+

+ + {"foo"} + +

+
+
+ "#, + Some(json!([{ "children": "always" }])), + ), + ( + " + +   +   + + ", + Some(json!([{ "children": "always" }])), + ), + ( + " + const Component2 = () => { + return /*; + }; + ", + None, + ), + ( + " + const Component2 = () => { + return /*; + }; + ", + Some(json!([{ "props": "never", "children": "never" }])), + ), + ( + r#" + import React from "react"; + + const Component = () => { + return {"/*"}; + }; + "#, + Some(json!([{ "props": "never", "children": "never" }])), + ), + ("{/* comment */}", None), + (" />", None), + ("} />", None), + (" />", Some(json!([{ "propElementValues": "ignore" }]))), + ("} />", Some(json!([{ "propElementValues": "ignore" }]))), + ( + r#" + + "#, + None, + ), + ( + r#" + {activity.type}} + /> + "#, + Some(json!(["never"])), + ), + ("", Some(json!(["never"]))), + ("{`${label}`}", Some(json!(["never"]))), + ]; + + let fail = vec![ + ("", Some(json!([{ "props": "never" }]))), + ("{}", Some(json!([{ "children": "never" }]))), + ("{}", None), + ("foo", Some(json!([{ "props": "never" }]))), + ("{`foo`}", Some(json!([{ "children": "never" }]))), + ("<>{`foo`}", Some(json!([{ "children": "never" }]))), + ("{'foo'}", None), + ("foo", None), + ("{'foo'}", Some(json!([{ "children": "never" }]))), + ("foo", Some(json!([{ "props": "never" }]))), + ( + " + + {'%'} + + ", + Some(json!([{ "children": "never" }])), + ), + ( + " + + {'foo'} +
+ {'bar'} +
+ {'baz'} +
+ ", + Some(json!([{ "children": "never" }])), + ), + ( + " + + {'foo'} +
+ {'bar'} +
+ {'baz'} + {'some-complicated-exp'} +
+ ", + Some(json!([{ "children": "never" }])), + ), + ("foo", Some(json!([{ "props": "always" }]))), + ( + r#"foo"#, + Some(json!([{ "props": "always" }])), + ), + ( + r#"foo"#, + Some(json!([{ "props": "always" }])), + ), + ( + r#"foo"#, + Some(json!([{ "props": "always" }])), + ), + ("foo bar ", Some(json!([{ "children": "always" }]))), + ( + r#"foo"#, + Some(json!([{ "props": "always" }])), + ), + ("foo bar \r ", Some(json!([{ "children": "always" }]))), + ("foo bar 'foo'", Some(json!([{ "children": "always" }]))), + (r#"foo bar "foo""#, Some(json!([{ "children": "always" }]))), + // NOTE: Not sure how to handle this case + // ("foo bar ", Some(json!([{ "children": "always" }]))), + ("foo \n bar", Some(json!([{ "children": "always" }]))), + ("foo \\u1234 bar", Some(json!([{ "children": "always" }]))), + ("", Some(json!([{ "props": "always" }]))), + ("{'foo'}", Some(json!(["never"]))), + ("foo", Some(json!(["always"]))), + (r#""#, Some(json!([{ "props": "never" }]))), + (r#""#, Some(json!([{ "props": "always" }]))), + (r#""#, Some(json!([{ "props": "always" }]))), + ("", Some(json!([{ "props": "always" }]))), + ("", Some(json!([{ "props": "always" }]))), + ("foo · bar", Some(json!([{ "children": "always" }]))), + (r#"{'foo "bar"'}"#, Some(json!([{ "children": "never" }]))), + (r#"{"foo 'bar'"}"#, Some(json!([{ "children": "never" }]))), + ( + r#" + + ", + Some(json!([{ "props": "never" }])), + ), + ( + " + + ", + Some(json!(["never"])), + ), + (r#"bar"#, Some(json!(["never"]))), + (r#""}>foo"#, Some(json!(["never"]))), + ( + " />", + Some( + json!([{ "props": "always", "children": "always", "propElementValues": "always" }]), + ), + ), + ( + "} />", + Some(json!([{ "props": "never", "children": "never", "propElementValues": "never" }])), + ), + ]; + + Tester::new(JsxCurlyBracePresence::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap b/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap new file mode 100644 index 0000000000000..9d38b5b3e2856 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/jsx_curly_brace_presence.snap @@ -0,0 +1,403 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:12] + 1 โ”‚ + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:7] + 1 โ”‚ {} + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:7] + 1 โ”‚ {} + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:12] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:7] + 1 โ”‚ {`foo`} + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:4] + 1 โ”‚ <>{`foo`} + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:15] + 1 โ”‚ {'foo'} + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:20] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:15] + 1 โ”‚ {'foo'} + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:20] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:3:15] + 2 โ”‚ + 3 โ”‚ {'%'} + ยท โ”€โ”€โ”€ + 4 โ”‚ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:3:15] + 2 โ”‚ + 3 โ”‚ {'foo'} + ยท โ”€โ”€โ”€โ”€โ”€ + 4 โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:7:15] + 6 โ”‚
+ 7 โ”‚ {'baz'} + ยท โ”€โ”€โ”€โ”€โ”€ + 8 โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:5:17] + 4 โ”‚
+ 5 โ”‚ {'bar'} + ยท โ”€โ”€โ”€โ”€โ”€ + 6 โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:3:15] + 2 โ”‚ + 3 โ”‚ {'foo'} + ยท โ”€โ”€โ”€โ”€โ”€ + 4 โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:7:15] + 6 โ”‚
+ 7 โ”‚ {'baz'} + ยท โ”€โ”€โ”€โ”€โ”€ + 8 โ”‚ {'some-complicated-exp'} + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:8:15] + 7 โ”‚ {'baz'} + 8 โ”‚ {'some-complicated-exp'} + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 9 โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:5:17] + 4 โ”‚
+ 5 โ”‚ {'bar'} + ยท โ”€โ”€โ”€โ”€โ”€ + 6 โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ foo + ยท โ”€โ”€โ”ฌโ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ foo bar + ยท โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ foo + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ foo bar + ยท โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ foo bar 'foo' + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ foo bar "foo" + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ โ•ญโ”€โ–ถ foo + 2 โ”‚ โ”œโ”€โ–ถ bar + ยท โ•ฐโ”€โ”€โ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ foo \u1234 bar + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:20] + 1 โ”‚ {'foo'} + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:28] + 1 โ”‚ {'foo'} + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:19] + 1 โ”‚ foo + ยท โ”€โ”€โ”ฌโ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:25] + 1 โ”‚ foo + ยท โ”€โ”ฌโ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:12] + 1 โ”‚ + ยท โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:11] + 1 โ”‚ + ยท โ”€โ”€โ”ฌโ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:22] + 1 โ”‚ + ยท โ”€โ”€โ”ฌโ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:11] + 1 โ”‚ + ยท โ”€โ”€โ”ฌโ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:24] + 1 โ”‚ + ยท โ”€โ”€โ”ฌโ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:11] + 1 โ”‚ + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:6] + 1 โ”‚ foo · bar + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:7] + 1 โ”‚ {'foo "bar"'} + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:7] + 1 โ”‚ {"foo 'bar'"} + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Unterminated string + โ•ญโ”€[jsx_curly_brace_presence.tsx:2:22] + 1 โ”‚ + 2 โ”‚ bar + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:20] + 1 โ”‚ "}>foo + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:13] + 1 โ”‚ /> + ยท โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€ + ยท โ•ฐโ”€โ”€ Wrap this value in curly braces + โ•ฐโ”€โ”€โ”€โ”€ + help: Wrap this value in curly braces + + โš  eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here. + โ•ญโ”€[jsx_curly_brace_presence.tsx:1:14] + 1 โ”‚ } /> + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€