feat(linter): add react/jsx-curly-brace-presence (#3949)

Note that this PR does not implement a fixer, but one is available.
This commit is contained in:
Don Isaac 2024-07-30 22:30:55 -04:00 committed by GitHub
parent eaf834f953
commit 85e8418a78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1367 additions and 0 deletions

View file

@ -370,6 +370,11 @@ pub enum JSXChild<'a> {
/// `<Foo>{...spread}</Foo>`
Spread(Box<'a, JSXSpreadChild<'a>>),
}
impl<'a> JSXChild<'a> {
pub const fn is_expression_container(&self) -> bool {
matches!(self, Self::ExpressionContainer(_))
}
}
/// JSX Spread Child.
///

View file

@ -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,

View file

@ -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<Self, Self::Error> {
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": [<enabled>, { "props": <string>, "children": <string>, "propElementValues": <string> }]
/// ...
/// ```
///
/// or alternatively
///
/// ```js
/// ...
/// "react/jsx-curly-brace-presence": [<enabled>, <string>]
/// ...
/// ```
///
/// ### Valid options for `<string>`
///
/// 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
/// <App>Hello world</App>;
/// <App prop='Hello world'>{'Hello world'}</App>;
/// ```
///
/// They can be fixed to:
///
/// ```jsx
/// <App>{"Hello world"}</App>;
/// <App prop={"Hello world"}>{'Hello world'}</App>;
/// ```
///
/// Examples of **incorrect** code for this rule, when configured with `{ props: "never", children: "never" }`:
///
/// ```jsx
/// <App>{'Hello world'}</App>;
/// <App prop={'Hello world'} attr={"foo"} />;
/// ```
///
/// They can be fixed to:
///
/// ```jsx
/// <App>Hello world</App>;
/// <App prop="Hello world" attr="foo" />;
/// ```
///
/// Examples of **incorrect** code for this rule, when configured with `{ props: "always", children: "always", "propElementValues": "always" }`:
///
/// ```jsx
/// <App prop=<div /> />;
/// ```
///
/// They can be fixed to:
///
/// ```jsx
/// <App prop={<div />} />;
/// ```
///
/// Examples of **incorrect** code for this rule, when configured with `{ props: "never", children: "never", "propElementValues": "never" }`:
///
/// ```jsx
/// <App prop={<div />} />;
/// ```
///
/// They can be fixed to:
///
/// ```jsx
/// <App prop=<div /> />;
/// ```
///
/// ### 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
/// <App>Hello world</App>;
/// <App prop='Hello world' attr="foo">Hello world</App>;
/// ```
///
/// They can be fixed to:
///
/// ```jsx
/// <App>{"Hello world"}</App>;
/// <App prop={"Hello world"} attr={"foo"}>{"Hello world"}</App>;
/// ```
///
/// Examples of **incorrect** code for this rule, when configured with `"never"`:
///
/// ```jsx
/// <App prop={'foo'} attr={"bar"}>{'Hello world'}</App>;
/// ```
///
/// It can fixed to:
///
/// ```jsx
/// <App prop="foo" attr="bar">Hello world</App>;
/// ```
///
/// ## 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
/// <App prop={`Hello world`}>{`Hello world`}</App>;
/// ```
///
/// will be warned and fixed to:
///
/// ```jsx
/// <App prop="Hello world">Hello world</App>;
/// ```
///
/// - 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
/// <App prop='Hello "foo" world'>Hello 'foo' "bar" world</App>;
/// ```
///
/// will warned and fixed to:
///
/// ```jsx
/// <App prop={"Hello \"foo\" world"}>{"Hello 'foo' \"bar\" world"}</App>;
/// ```
///
/// - 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
/// <Color text={"\u00a0"} />
/// <App>{"Hello \u00b7 world"}</App>;
/// <style type="text/css">{'.main { margin-top: 0; }'}</style>;
/// /**
/// * there's no way to inject a whitespace into jsx without a container so this
/// * will always be allowed.
/// */
/// <App>{' '}</App>
/// <App>{' '}</App>
/// <App>{/* comment */ <Bpp />}</App> // 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![
("<App {...props}>foo</App>", None),
("<>foo</>", None),
("<App {...props}>foo</App>", Some(json!([{ "props": "never" }]))),
("<App>{' '}</App>", None),
(
"<App>{' '}
</App>",
None,
),
("<App>{' '}</App>", None),
(
"<App>{' '}
</App>",
None,
),
("<App>{' '}</App>", Some(json!([{ "children": "never" }]))),
("<App>{' '}</App>", Some(json!([{ "children": "never" }]))),
("<App>{' '}</App>", Some(json!([{ "children": "always" }]))),
("<App>{' '}</App>", Some(json!([{ "children": "always" }]))),
("<App {...props}>foo</App>", Some(json!([{ "props": "always" }]))),
("<App>{`Hello ${word} World`}</App>", Some(json!([{ "children": "never" }]))),
(
"
<React.Fragment>
foo{' '}
<span>bar</span>
</React.Fragment>
",
Some(json!([{ "children": "never" }])),
),
(
"
<>
foo{' '}
<span>bar</span>
</>
",
Some(json!([{ "children": "never" }])),
),
("<App>{`Hello \n World`}</App>", Some(json!([{ "children": "never" }]))),
("<App>{`Hello ${word} World`}{`foo`}</App>", Some(json!([{ "children": "never" }]))),
("<App prop={`foo ${word} bar`}>foo</App>", Some(json!([{ "props": "never" }]))),
("<App prop={`foo ${word} bar`} />", Some(json!([{ "props": "never" }]))),
("<App>{<myApp></myApp>}</App>", Some(json!([{ "children": "always" }]))),
("<App>{[]}</App>", None),
("<App>foo</App>", None),
(r#"<App>{"foo"}{<Component>bar</Component>}</App>"#, None),
("<App prop='bar'>foo</App>", None),
("<App prop={true}>foo</App>", None),
("<App prop>foo</App>", None),
(r"<App prop='bar'>{'foo \n bar'}</App>", None),
("<App prop={ ' ' }/>", None),
("<MyComponent prop='bar'>foo</MyComponent>", Some(json!([{ "props": "never" }]))),
(r#"<MyComponent prop="bar">foo</MyComponent>"#, Some(json!([{ "props": "never" }]))),
("<MyComponent>foo</MyComponent>", Some(json!([{ "children": "never" }]))),
(r#"<MyComponent>{<App/>}{"123"}</MyComponent>"#, Some(json!([{ "children": "never" }]))),
(r#"<App>{"foo 'bar' \"foo\" bar"}</App>"#, Some(json!([{ "children": "never" }]))),
("<MyComponent prop={'bar'}>foo</MyComponent>", Some(json!([{ "props": "always" }]))),
("<MyComponent>{'foo'}</MyComponent>", Some(json!([{ "children": "always" }]))),
(r#"<MyComponent prop={"bar"}>foo</MyComponent>"#, Some(json!([{ "props": "always" }]))),
(r#"<MyComponent>{"foo"}</MyComponent>"#, Some(json!([{ "children": "always" }]))),
("<MyComponent>{'foo'}</MyComponent>", Some(json!([{ "children": "ignore" }]))),
("<MyComponent prop={'bar'}>foo</MyComponent>", Some(json!([{ "props": "ignore" }]))),
("<MyComponent>foo</MyComponent>", Some(json!([{ "children": "ignore" }]))),
("<MyComponent prop='bar'>foo</MyComponent>", Some(json!([{ "props": "ignore" }]))),
(r#"<MyComponent prop="bar">foo</MyComponent>"#, Some(json!([{ "props": "ignore" }]))),
(
"<MyComponent prop='bar'>{'foo'}</MyComponent>",
Some(json!([{ "children": "always", "props": "never" }])),
),
(
"<MyComponent prop={'bar'}>foo</MyComponent>",
Some(json!([{ "children": "never", "props": "always" }])),
),
("<MyComponent prop={'bar'}>{'foo'}</MyComponent>", Some(json!(["always"]))),
(r#"<MyComponent prop={"bar"}>{"foo"}</MyComponent>"#, Some(json!(["always"]))),
(r#"<MyComponent prop={"bar"} attr={'foo'} />"#, Some(json!(["always"]))),
(r#"<MyComponent prop="bar" attr='foo' />"#, Some(json!(["never"]))),
("<MyComponent prop='bar'>foo</MyComponent>", Some(json!(["never"]))),
(
"<MyComponent prop={`bar ${word} foo`}>{`foo ${word}`}</MyComponent>",
Some(json!(["never"])),
),
(r#"<MyComponent>{"div { margin-top: 0; }"}</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent>{"<Foo />"}</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent prop={"Hello \u1026 world"}>bar</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent>{"Hello \u1026 world"}</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent prop={"Hello &middot; world"}>bar</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent>{"Hello &middot; world"}</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent>{"Hello \n world"}</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent>{"space after "}</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent>{" space before"}</MyComponent>"#, Some(json!(["never"]))),
("<MyComponent>{`space after `}</MyComponent>", Some(json!(["never"]))),
("<MyComponent>{` space before`}</MyComponent>", Some(json!(["never"]))),
(
"
<App prop={`
a
b
`} />
",
Some(json!(["never"])),
),
(
"
<App prop={`
a
b
`} />
",
Some(json!(["always"])),
),
(
"
<App>
{`
a
b
`}
</App>
",
Some(json!(["never"])),
),
(
"
<App>{`
a
b
`}</App>
",
Some(json!(["always"])),
),
(
"
<MyComponent>
%
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
(
"
<MyComponent>
{ 'space after ' }
<b>foo</b>
{ ' space before' }
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
(
"
<MyComponent>
{ `space after ` }
<b>foo</b>
{ ` space before` }
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
(
"
<MyComponent>
foo
<div>bar</div>
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
(
"
<MyComponent p={<Foo>Bar</Foo>}>
</MyComponent>
",
None,
),
(
r#"
<MyComponent>
<div>
<p>
<span>
{"foo"}
</span>
</p>
</div>
</MyComponent>
"#,
Some(json!([{ "children": "always" }])),
),
(
"
<App>
<Component />&nbsp;
&nbsp;
</App>
",
Some(json!([{ "children": "always" }])),
),
(
"
const Component2 = () => {
return <span>/*</span>;
};
",
None,
),
(
"
const Component2 = () => {
return <span>/*</span>;
};
",
Some(json!([{ "props": "never", "children": "never" }])),
),
(
r#"
import React from "react";
const Component = () => {
return <span>{"/*"}</span>;
};
"#,
Some(json!([{ "props": "never", "children": "never" }])),
),
("<App>{/* comment */}</App>", None),
("<App horror=<div /> />", None),
("<App horror={<div />} />", None),
("<App horror=<div /> />", Some(json!([{ "propElementValues": "ignore" }]))),
("<App horror={<div />} />", Some(json!([{ "propElementValues": "ignore" }]))),
(
r#"
<script>{`window.foo = "bar"`}</script>
"#,
None,
),
(
r#"
<CollapsibleTitle
extra={<span className="activity-type">{activity.type}</span>}
/>
"#,
Some(json!(["never"])),
),
("<App label={`${label}`} />", Some(json!(["never"]))),
("<App>{`${label}`}</App>", Some(json!(["never"]))),
];
let fail = vec![
("<App prop={`foo`} />", Some(json!([{ "props": "never" }]))),
("<App>{<myApp></myApp>}</App>", Some(json!([{ "children": "never" }]))),
("<App>{<myApp></myApp>}</App>", None),
("<App prop={`foo`}>foo</App>", Some(json!([{ "props": "never" }]))),
("<App>{`foo`}</App>", Some(json!([{ "children": "never" }]))),
("<>{`foo`}</>", Some(json!([{ "children": "never" }]))),
("<MyComponent>{'foo'}</MyComponent>", None),
("<MyComponent prop={'bar'}>foo</MyComponent>", None),
("<MyComponent>{'foo'}</MyComponent>", Some(json!([{ "children": "never" }]))),
("<MyComponent prop={'bar'}>foo</MyComponent>", Some(json!([{ "props": "never" }]))),
(
"
<MyComponent>
{'%'}
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
(
"
<MyComponent>
{'foo'}
<div>
{'bar'}
</div>
{'baz'}
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
(
"
<MyComponent>
{'foo'}
<div>
{'bar'}
</div>
{'baz'}
{'some-complicated-exp'}
</MyComponent>
",
Some(json!([{ "children": "never" }])),
),
("<MyComponent prop='bar'>foo</MyComponent>", Some(json!([{ "props": "always" }]))),
(
r#"<MyComponent prop="foo 'bar'">foo</MyComponent>"#,
Some(json!([{ "props": "always" }])),
),
(
r#"<MyComponent prop='foo "bar"'>foo</MyComponent>"#,
Some(json!([{ "props": "always" }])),
),
(
r#"<MyComponent prop="foo 'bar'">foo</MyComponent>"#,
Some(json!([{ "props": "always" }])),
),
("<MyComponent>foo bar </MyComponent>", Some(json!([{ "children": "always" }]))),
(
r#"<MyComponent prop="foo 'bar' \n ">foo</MyComponent>"#,
Some(json!([{ "props": "always" }])),
),
("<MyComponent>foo bar \r </MyComponent>", Some(json!([{ "children": "always" }]))),
("<MyComponent>foo bar 'foo'</MyComponent>", Some(json!([{ "children": "always" }]))),
(r#"<MyComponent>foo bar "foo"</MyComponent>"#, Some(json!([{ "children": "always" }]))),
// NOTE: Not sure how to handle this case
// ("<MyComponent>foo bar <App/></MyComponent>", Some(json!([{ "children": "always" }]))),
("<MyComponent>foo \n bar</MyComponent>", Some(json!([{ "children": "always" }]))),
("<MyComponent>foo \\u1234 bar</MyComponent>", Some(json!([{ "children": "always" }]))),
("<MyComponent prop='foo \\u1234 bar' />", Some(json!([{ "props": "always" }]))),
("<MyComponent prop={'bar'}>{'foo'}</MyComponent>", Some(json!(["never"]))),
("<MyComponent prop='bar'>foo</MyComponent>", Some(json!(["always"]))),
(r#"<App prop={'foo'} attr={" foo "} />"#, Some(json!([{ "props": "never" }]))),
(r#"<App prop='foo' attr="bar" />"#, Some(json!([{ "props": "always" }]))),
(r#"<App prop='foo' attr={"bar"} />"#, Some(json!([{ "props": "always" }]))),
("<App prop={'foo'} attr='bar' />", Some(json!([{ "props": "always" }]))),
("<App prop='foo &middot; bar' />", Some(json!([{ "props": "always" }]))),
("<App>foo &middot; bar</App>", Some(json!([{ "children": "always" }]))),
(r#"<App>{'foo "bar"'}</App>"#, Some(json!([{ "children": "never" }]))),
(r#"<App>{"foo 'bar'"}</App>"#, Some(json!([{ "children": "never" }]))),
(
r#"
<App prop=""#,
Some(json!(["always"])),
),
(
"
<App prop='",
Some(json!(["always"])),
),
(
"
<App>
foo bar
<div>foo bar foo</div>
<span>
foo bar <i>foo bar</i>
<strong>
foo bar
</strong>
</span>
</App>
",
Some(json!([{ "children": "always" }])),
),
// NOTE: Not sure how to handle this case
// (
// "
// <App>
// &lt;Component&gt;
// &nbsp;<Component />&nbsp;
// &nbsp;
// </App>
// ",
// Some(json!([{ "children": "always" }])),
// ),
(
"
<Box mb={'1rem'} />
",
Some(json!([{ "props": "never" }])),
),
(
"
<Box mb={'1rem {}'} />
",
Some(json!(["never"])),
),
(r#"<MyComponent prop={"{ style: true }"}>bar</MyComponent>"#, Some(json!(["never"]))),
(r#"<MyComponent prop={"< style: true >"}>foo</MyComponent>"#, Some(json!(["never"]))),
(
"<App horror=<div /> />",
Some(
json!([{ "props": "always", "children": "always", "propElementValues": "always" }]),
),
),
(
"<App horror={<div />} />",
Some(json!([{ "props": "never", "children": "never", "propElementValues": "never" }])),
),
];
Tester::new(JsxCurlyBracePresence::NAME, pass, fail).test_and_snapshot();
}

View file

@ -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 │ <App prop={`foo`} />
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:7]
1 │ <App>{<myApp></myApp>}</App>
· ───────────────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:7]
1 │ <App>{<myApp></myApp>}</App>
· ───────────────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:12]
1 │ <App prop={`foo`}>foo</App>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:7]
1 │ <App>{`foo`}</App>
· ─────
╰────
⚠ 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 │ <MyComponent>{'foo'}</MyComponent>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:20]
1 │ <MyComponent prop={'bar'}>foo</MyComponent>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:15]
1 │ <MyComponent>{'foo'}</MyComponent>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:20]
1 │ <MyComponent prop={'bar'}>foo</MyComponent>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:3:15]
2 │ <MyComponent>
3 │ {'%'}
· ───
4 │ </MyComponent>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:3:15]
2 │ <MyComponent>
3 │ {'foo'}
· ─────
4 │ <div>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:7:15]
6 │ </div>
7 │ {'baz'}
· ─────
8 │ </MyComponent>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:5:17]
4 │ <div>
5 │ {'bar'}
· ─────
6 │ </div>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:3:15]
2 │ <MyComponent>
3 │ {'foo'}
· ─────
4 │ <div>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:7:15]
6 │ </div>
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 │ </MyComponent>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:5:17]
4 │ <div>
5 │ {'bar'}
· ─────
6 │ </div>
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:1:19]
1 │ <MyComponent prop='bar'>foo</MyComponent>
· ──┬──
· ╰── 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 │ <MyComponent prop="foo 'bar'">foo</MyComponent>
· ─────┬─────
· ╰── 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 │ <MyComponent prop='foo "bar"'>foo</MyComponent>
· ─────┬─────
· ╰── 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 │ <MyComponent prop="foo 'bar'">foo</MyComponent>
· ─────┬─────
· ╰── 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 │ <MyComponent>foo bar </MyComponent>
· ────┬───
· ╰── 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 │ <MyComponent prop="foo 'bar' \n ">foo</MyComponent>
· ───────┬───────
· ╰── 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 │ <MyComponent>foo bar </MyComponent>
· ────┬────
· ╰── 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 │ <MyComponent>foo bar 'foo'</MyComponent>
· ──────┬──────
· ╰── 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 │ <MyComponent>foo bar "foo"</MyComponent>
· ──────┬──────
· ╰── 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 │ ╭─▶ <MyComponent>foo
2 │ ├─▶ bar</MyComponent>
· ╰──── 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 │ <MyComponent>foo \u1234 bar</MyComponent>
· ───────┬──────
· ╰── 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 │ <MyComponent prop='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 unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:20]
1 │ <MyComponent prop={'bar'}>{'foo'}</MyComponent>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:28]
1 │ <MyComponent prop={'bar'}>{'foo'}</MyComponent>
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:1:19]
1 │ <MyComponent prop='bar'>foo</MyComponent>
· ──┬──
· ╰── 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 │ <MyComponent prop='bar'>foo</MyComponent>
· ─┬─
· ╰── 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 │ <App prop={'foo'} attr={" foo "} />
· ─────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:1:11]
1 │ <App prop='foo' attr="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:22]
1 │ <App prop='foo' attr="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:11]
1 │ <App prop='foo' attr={"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:24]
1 │ <App prop={'foo'} attr='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:11]
1 │ <App prop='foo &middot; 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:6]
1 │ <App>foo &middot; bar</App>
· ────────┬───────
· ╰── 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 │ <App>{'foo "bar"'}</App>
· ───────────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:7]
1 │ <App>{"foo 'bar'"}</App>
· ───────────
╰────
× Unterminated string
╭─[jsx_curly_brace_presence.tsx:2:22]
1 │
2 │ <App prop="
· ─
╰────
× Unterminated string
╭─[jsx_curly_brace_presence.tsx:2:22]
1 │
2 │ <App prop='
· ─
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:4:19]
3 │ foo bar
4 │ <div>foo bar foo</div>
· ─────┬─────
· ╰── Wrap this value in curly braces
5 │ <span>
╰────
help: Wrap this value in curly braces
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:6:27]
5 │ <span>
6 │ foo bar <i>foo bar</i>
· ───┬───
· ╰── Wrap this value in curly braces
7 │ <strong>
╰────
help: Wrap this value in curly braces
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:7:24]
6 │ foo bar <i>foo bar</i>
7 │ ╭─▶ <strong>
8 │ │ foo bar
9 │ ├─▶ </strong>
· ╰──── Wrap this value in curly braces
10 │ </span>
╰────
help: Wrap this value in curly braces
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:2:21]
1 │
2 │ <Box mb={'1rem'} />
· ──────
3 │
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:2:27]
1 │
2 │ <Box mb={'1rem {}'} />
· ─────────
3 │
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:20]
1 │ <MyComponent prop={"{ style: true }"}>bar</MyComponent>
· ─────────────────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are unnecessary here.
╭─[jsx_curly_brace_presence.tsx:1:20]
1 │ <MyComponent prop={"< style: true >"}>foo</MyComponent>
· ─────────────────
╰────
⚠ eslint-plugin-react(jsx-curly-brace-presence): Curly braces are required here.
╭─[jsx_curly_brace_presence.tsx:1:13]
1 │ <App horror=<div /> />
· ───┬───
· ╰── 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 │ <App horror={<div />} />
· ───────
╰────