Skip to content

Commit 0765f22

Browse files
author
Declan Vong
authored
feat(jsx): add multiLineParens=always (#304)
1 parent cc6e635 commit 0765f22

File tree

12 files changed

+292
-20
lines changed

12 files changed

+292
-20
lines changed

src/configuration/builder.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ impl ConfigurationBuilder {
6565
.tagged_template_space_before_literal(false)
6666
.conditional_expression_prefer_single_line(true)
6767
.quote_style(QuoteStyle::PreferDouble)
68-
.jsx_multi_line_parens(true)
68+
.jsx_multi_line_parens(JsxMultiLineParensStyle::Prefer)
6969
.ignore_node_comment_text("deno-fmt-ignore")
7070
.ignore_file_comment_text("deno-fmt-ignore-file")
7171
.module_sort_import_declarations(SortOrder::Maintain)
@@ -118,8 +118,8 @@ impl ConfigurationBuilder {
118118
/// when it's the top JSX node and it spans multiple lines.
119119
///
120120
/// Default: true
121-
pub fn jsx_multi_line_parens(&mut self, value: bool) -> &mut Self {
122-
self.insert("jsx.multiLineParens", value.into())
121+
pub fn jsx_multi_line_parens(&mut self, value: JsxMultiLineParensStyle) -> &mut Self {
122+
self.insert("jsx.multiLineParens", value.to_string().into())
123123
}
124124

125125
/// Whether statements should end in a semi-colon.
@@ -939,7 +939,7 @@ mod tests {
939939
/* common */
940940
.quote_style(QuoteStyle::AlwaysDouble)
941941
.jsx_quote_style(JsxQuoteStyle::PreferSingle)
942-
.jsx_multi_line_parens(false)
942+
.jsx_multi_line_parens(JsxMultiLineParensStyle::Never)
943943
.semi_colons(SemiColons::Prefer)
944944
.brace_position(BracePosition::NextLine)
945945
.next_control_flow_position(NextControlFlowPosition::SameLine)

src/configuration/resolve_config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
7777
arrow_function_use_parentheses: get_value(&mut config, "arrowFunction.useParentheses", UseParentheses::Maintain, &mut diagnostics),
7878
binary_expression_line_per_expression: get_value(&mut config, "binaryExpression.linePerExpression", false, &mut diagnostics),
7979
jsx_quote_style: get_value(&mut config, "jsx.quoteStyle", quote_style.to_jsx_quote_style(), &mut diagnostics),
80-
jsx_multi_line_parens: get_value(&mut config, "jsx.multiLineParens", true, &mut diagnostics),
80+
jsx_multi_line_parens: get_value(&mut config, "jsx.multiLineParens", JsxMultiLineParensStyle::Prefer, &mut diagnostics),
8181
member_expression_line_per_expression: get_value(&mut config, "memberExpression.linePerExpression", false, &mut diagnostics),
8282
type_literal_separator_kind_single_line: get_value(
8383
&mut config,

src/configuration/types.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,22 @@ pub enum JsxQuoteStyle {
206206

207207
generate_str_to_from![JsxQuoteStyle, [PreferDouble, "preferDouble"], [PreferSingle, "preferSingle"]];
208208

209+
/// Whether to surround a JSX element or fragment with parentheses
210+
/// when it's the top JSX node and it spans multiple lines.
211+
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
212+
#[serde(rename_all = "camelCase")]
213+
pub enum JsxMultiLineParensStyle {
214+
/// Never wrap JSX with parentheses.
215+
Never,
216+
/// Prefer wrapping with parentheses in most scenarios, except in function
217+
/// arguments and JSX attributes.
218+
Prefer,
219+
/// Always wrap JSX with parentheses if it spans multiple lines.
220+
Always,
221+
}
222+
223+
generate_str_to_from![JsxMultiLineParensStyle, [Never, "never"], [Prefer, "prefer"], [Always, "always"]];
224+
209225
/// Whether to use semi-colons or commas.
210226
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
211227
#[serde(rename_all = "camelCase")]
@@ -254,7 +270,7 @@ pub struct Configuration {
254270
#[serde(rename = "jsx.quoteStyle")]
255271
pub jsx_quote_style: JsxQuoteStyle,
256272
#[serde(rename = "jsx.multiLineParens")]
257-
pub jsx_multi_line_parens: bool,
273+
pub jsx_multi_line_parens: JsxMultiLineParensStyle,
258274
#[serde(rename = "memberExpression.linePerExpression")]
259275
pub member_expression_line_per_expression: bool,
260276
#[serde(rename = "typeLiteral.separatorKind.singleLine")]

src/parsing/parser.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3048,7 +3048,7 @@ fn handle_jsx_surrounding_parens<'a>(inner_items: PrintItems, context: &mut Cont
30483048
}
30493049
}
30503050

3051-
if context.parent().is::<JSXExprContainer>() {
3051+
if context.parent().is::<JSXExprContainer>() && context.config.jsx_multi_line_parens != JsxMultiLineParensStyle::Always {
30523052
return surround_with_newlines_indented_if_multi_line(inner_items, context.config.indent_width);
30533053
}
30543054

@@ -3093,31 +3093,37 @@ fn handle_jsx_surrounding_parens<'a>(inner_items: PrintItems, context: &mut Cont
30933093
}
30943094

30953095
fn is_jsx_paren_expr_handled_node(node: &Node, context: &Context) -> bool {
3096-
if !context.config.jsx_multi_line_parens {
3096+
if context.config.jsx_multi_line_parens == JsxMultiLineParensStyle::Never {
30973097
return false;
30983098
}
30993099

31003100
if !matches!(node.kind(), NodeKind::JSXElement | NodeKind::JSXFragment) {
31013101
return false;
31023102
}
31033103

3104+
let mut parent = node.parent().unwrap();
3105+
// Only wrap the top-level JSX element in parens
3106+
if matches!(parent.kind(), NodeKind::JSXElement | NodeKind::JSXFragment) {
3107+
return false;
3108+
}
3109+
31043110
if node_helpers::has_surrounding_comments(node, context.module) {
31053111
return false;
31063112
}
31073113

3108-
let mut parent = node.parent().unwrap();
31093114
while parent.is::<ParenExpr>() {
31103115
if node_helpers::has_surrounding_comments(&parent, context.module) {
31113116
return false;
31123117
}
31133118
parent = parent.parent().unwrap();
31143119
}
31153120

3121+
if context.config.jsx_multi_line_parens == JsxMultiLineParensStyle::Always {
3122+
return true;
3123+
}
3124+
31163125
// do not allow in expr statement, argument, attributes, or jsx exprs
3117-
!matches!(
3118-
parent.kind(),
3119-
NodeKind::ExprStmt | NodeKind::ExprOrSpread | NodeKind::JSXElement | NodeKind::JSXFragment | NodeKind::JSXExprContainer
3120-
)
3126+
!matches!(parent.kind(), NodeKind::ExprStmt | NodeKind::ExprOrSpread | NodeKind::JSXExprContainer)
31213127
}
31223128

31233129
fn parse_jsx_element<'a>(node: &'a JSXElement, context: &mut Context<'a>) -> PrintItems {

tests/specs/issues/issue0094.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- test.jsx --
2-
~~ lineWidth: 80, jsx.multiLineParens: false ~~
2+
~~ lineWidth: 80, jsx.multiLineParens: never ~~
33
== should format block comments in jsx expr container ==
44
const render = items => (
55
<div>{ /* eslint-disable */

tests/specs/issues/old-repo/issue131.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- file.tsx --
2-
~~ indentWidth: 2, jsx.multiLineParens: false ~~
2+
~~ indentWidth: 2, jsx.multiLineParens: never ~~
33
== should format as-is ==
44
const View = () => (
55
<div class="deno">land</div>
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
-- file.tsx --
2+
~~ lineWidth: 50, jsx.multiLineParens: always ~~
3+
== should remove paren expression when the element isn't multi-line ==
4+
const t = (
5+
<Test></Test>
6+
);
7+
8+
[expect]
9+
const t = <Test></Test>;
10+
11+
== should not remove paren expression when the element isn't multi-line, but has surrounding comments ==
12+
const t = ( // test
13+
<Test></Test>
14+
);
15+
const u = (
16+
// test
17+
<Test></Test>
18+
);
19+
const v = (
20+
<Test></Test> // test
21+
);
22+
const w = (
23+
<Test></Test>
24+
// test
25+
);
26+
27+
[expect]
28+
const t = ( // test
29+
<Test></Test>
30+
);
31+
const u = (
32+
// test
33+
<Test></Test>
34+
);
35+
const v = (
36+
<Test></Test> // test
37+
);
38+
const w = (
39+
<Test></Test>
40+
// test
41+
);
42+
43+
== should add parens around multi-line element in an expr stmt ==
44+
<Test><Test /></Test>;
45+
46+
function test() {
47+
<Test><Test /></Test>;
48+
}
49+
50+
[expect]
51+
(
52+
<Test>
53+
<Test />
54+
</Test>
55+
);
56+
57+
function test() {
58+
(
59+
<Test>
60+
<Test />
61+
</Test>
62+
);
63+
}
64+
65+
== should only add parens at the top level of a multi-line element ==
66+
<Test><Test><Test>some text</Test><br/>some more text</Test></Test>
67+
68+
[expect]
69+
(
70+
<Test>
71+
<Test>
72+
<Test>some text</Test>
73+
<br />some more text
74+
</Test>
75+
</Test>
76+
);
77+
78+
== should add parens around a JSX element that wraps ==
79+
expect(<SomeElement arg1={arg1} arg2={arg2} arg3={arg3} />).toMatchRenderedSnapshot();
80+
81+
[expect]
82+
expect(
83+
(
84+
<SomeElement
85+
arg1={arg1}
86+
arg2={arg2}
87+
arg3={arg3}
88+
/>
89+
),
90+
).toMatchRenderedSnapshot();
91+
92+
== should add parens around an element in an argument ==
93+
ReactDOM.render(
94+
<React.StrictMode>
95+
<App />
96+
</React.StrictMode>,
97+
document.getElementById("root"),
98+
);
99+
new MyClass(
100+
<React.StrictMode>
101+
<App />
102+
</React.StrictMode>,
103+
);
104+
105+
[expect]
106+
ReactDOM.render(
107+
(
108+
<React.StrictMode>
109+
<App />
110+
</React.StrictMode>
111+
),
112+
document.getElementById("root"),
113+
);
114+
new MyClass(
115+
(
116+
<React.StrictMode>
117+
<App />
118+
</React.StrictMode>
119+
),
120+
);
121+
122+
== should surround element in jsx expression with parens ==
123+
const t = (
124+
<A
125+
icon={
126+
<Testing>
127+
This out
128+
<Test />
129+
</Testing>
130+
}
131+
/>
132+
);
133+
const u = (
134+
<A
135+
icon={(
136+
<Testing>
137+
This out
138+
<Test />
139+
</Testing>
140+
)}
141+
/>
142+
);
143+
const v = (
144+
<A
145+
icon={<Testing>
146+
This out
147+
<Test />
148+
</Testing>}
149+
/>
150+
);
151+
152+
153+
[expect]
154+
const t = (
155+
<A
156+
icon={(
157+
<Testing>
158+
This out
159+
<Test />
160+
</Testing>
161+
)}
162+
/>
163+
);
164+
const u = (
165+
<A
166+
icon={(
167+
<Testing>
168+
This out
169+
<Test />
170+
</Testing>
171+
)}
172+
/>
173+
);
174+
const v = (
175+
<A
176+
icon={(
177+
<Testing>
178+
This out
179+
<Test />
180+
</Testing>
181+
)}
182+
/>
183+
);
184+
185+
== should remove many nested parens ==
186+
const t = (
187+
<A
188+
icon={(((
189+
<Testing>
190+
This out
191+
<Test />
192+
</Testing>
193+
)))}
194+
/>
195+
);
196+
197+
[expect]
198+
const t = (
199+
<A
200+
icon={(
201+
<Testing>
202+
This out
203+
<Test />
204+
</Testing>
205+
)}
206+
/>
207+
);
208+
209+
== should not remove parens when has comment ==
210+
const t = (
211+
<A
212+
icon={(
213+
// test
214+
((
215+
<Testing>
216+
This out
217+
<Test />
218+
</Testing>
219+
)))}
220+
/>
221+
);
222+
223+
[expect]
224+
const t = (
225+
<A
226+
icon={(
227+
// test
228+
<Testing>
229+
This out
230+
<Test />
231+
</Testing>
232+
)}
233+
/>
234+
);
235+
236+
== should handle jsx in arrow function expr with multiple parens ==
237+
const t = test.map((a, b) => <testing a={b} b={test}>
238+
{a[1]}
239+
</testing>);
240+
const u = test.map((a, b) => <testing a={b} b={test} />);
241+
242+
[expect]
243+
const t = test.map((a, b) => (
244+
<testing a={b} b={test}>
245+
{a[1]}
246+
</testing>
247+
));
248+
const u = test.map((a, b) => (
249+
<testing a={b} b={test} />
250+
));

tests/specs/jsx/JsxElement/JsxElement_MultiLineParens_False.txt renamed to tests/specs/jsx/JsxElement/JsxElement_MultiLineParens_Never.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- file.tsx --
2-
~~ lineWidth: 50, jsx.multiLineParens: false ~~
2+
~~ lineWidth: 50, jsx.multiLineParens: never ~~
33
== should format when multi line ==
44
const t = <Test>
55
Test</Test>;

0 commit comments

Comments
 (0)