Skip to content

Commit 1e4e565

Browse files
authored
feat: add "quoteProps" configuration (#330)
1 parent 3c78ad2 commit 1e4e565

File tree

6 files changed

+365
-1
lines changed

6 files changed

+365
-1
lines changed

Diff for: deployment/schema.json

+15
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@
6060
"description": "Prefers using single quotes except in scenarios where the string contains more single quotes than double quotes."
6161
}]
6262
},
63+
"quoteProps": {
64+
"description": "Change when properties in objects are quoted.",
65+
"type": "string",
66+
"default": "preserve",
67+
"oneOf": [{
68+
"const": "preserve",
69+
"description": "Preserve quotes around property names."
70+
}, {
71+
"const": "asNeeded",
72+
"description": "Remove unnecessary quotes around property names."
73+
}]
74+
},
6375
"jsx.multiLineParens": {
6476
"description": "Surrounds the top-most JSX element or fragment in parentheses when it spans multiple lines.",
6577
"type": "string",
@@ -679,6 +691,9 @@
679691
"quoteStyle": {
680692
"$ref": "#/definitions/quoteStyle"
681693
},
694+
"quoteProps": {
695+
"$ref": "#/definitions/quoteProps"
696+
},
682697
"newLineKind": {
683698
"$ref": "#/definitions/newLineKind"
684699
},

Diff for: src/configuration/resolve_config.rs

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
4545
let space_surrounding_properties = get_value(&mut config, "spaceSurroundingProperties", true, &mut diagnostics);
4646
let type_literal_separator_kind = get_value(&mut config, "typeLiteral.separatorKind", SemiColonOrComma::SemiColon, &mut diagnostics);
4747
let quote_style = get_value(&mut config, "quoteStyle", QuoteStyle::AlwaysDouble, &mut diagnostics);
48+
let quote_props = get_value(&mut config, "quoteProps", QuoteProps::Preserve, &mut diagnostics);
4849

4950
let resolved_config = Configuration {
5051
line_width: get_value(
@@ -72,6 +73,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
7273
&mut diagnostics,
7374
),
7475
quote_style,
76+
quote_props,
7577
semi_colons,
7678
/* situational */
7779
arrow_function_use_parentheses: get_value(&mut config, "arrowFunction.useParentheses", UseParentheses::Maintain, &mut diagnostics),

Diff for: src/configuration/types.rs

+18
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,23 @@ pub enum JsxQuoteStyle {
206206

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

209+
/// How to decide to use single or double quotes.
210+
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
211+
#[serde(rename_all = "camelCase")]
212+
pub enum QuoteProps {
213+
/// Preserve quotes around property names.
214+
Preserve,
215+
/// Remove unnecessary quotes around property names.
216+
AsNeeded,
217+
}
218+
219+
generate_str_to_from![
220+
QuoteProps,
221+
[Preserve, "preserve"],
222+
[AsNeeded, "asNeeded"]
223+
];
224+
225+
209226
/// Whether to surround a JSX element or fragment with parentheses
210227
/// when it's the top JSX node and it spans multiple lines.
211228
#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
@@ -261,6 +278,7 @@ pub struct Configuration {
261278
pub use_tabs: bool,
262279
pub new_line_kind: NewLineKind,
263280
pub quote_style: QuoteStyle,
281+
pub quote_props: QuoteProps,
264282
pub semi_colons: SemiColons,
265283
/* situational */
266284
#[serde(rename = "arrowFunction.useParentheses")]

Diff for: src/generation/generate.rs

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use deno_ast::swc::common::comments::{Comment, CommentKind};
22
use deno_ast::swc::common::{BytePos, Span, Spanned};
3+
use deno_ast::swc::parser::lexer::util::CharExt;
34
use deno_ast::swc::parser::token::{Token, TokenAndSpan};
45
use deno_ast::view::*;
56
use deno_ast::MediaType;
@@ -3486,10 +3487,48 @@ fn gen_string_literal<'a>(node: &'a Str, context: &mut Context<'a>) -> PrintItem
34863487
return gen_from_raw_string(&get_string_literal_text(
34873488
get_string_value(&node, context),
34883489
node.parent().is::<JSXAttr>(),
3490+
context.config.quote_props == QuoteProps::AsNeeded && is_property_name(&node),
34893491
context,
34903492
));
34913493

3492-
fn get_string_literal_text(string_value: String, is_jsx_attribute: bool, context: &mut Context) -> String {
3494+
fn is_property_name(node: &Str) -> bool {
3495+
match node.parent() {
3496+
Node::KeyValueProp(parent) => match_key_prop_name(parent.key),
3497+
Node::GetterProp(parent) => match_key_prop_name(parent.key),
3498+
Node::SetterProp(parent) => match_key_prop_name(parent.key),
3499+
Node::MethodProp(parent) => match_key_prop_name(parent.key),
3500+
// Do not match class properties Node::ClassProp
3501+
// With `--strictPropertyInitialization`, TS treats properties with quoted names differently than unquoted ones.
3502+
// See https://github.com/microsoft/TypeScript/pull/20075
3503+
// See prettier implementation https://github.com/prettier/prettier/blob/514046b3c70e6477cb69b4d36871d0d4c8c5e415/src/language-js/utils.js#L671-L716
3504+
Node::ClassMethod(parent) => match_key_prop_name(parent.key),
3505+
Node::TsPropertySignature(parent) => match_key_expr(parent.key),
3506+
Node::TsGetterSignature(parent) => match_key_expr(parent.key),
3507+
Node::TsSetterSignature(parent) => match_key_expr(parent.key),
3508+
Node::TsMethodSignature(parent) => match_key_expr(parent.key),
3509+
_ => false
3510+
}
3511+
}
3512+
3513+
fn match_key_expr(key: Expr) -> bool {
3514+
match key {
3515+
Expr::Lit(Lit::Str(_str)) => true,
3516+
Expr::Tpl(_tpl) => true,
3517+
_ => false
3518+
}
3519+
}
3520+
fn match_key_prop_name(key: PropName) -> bool {
3521+
match key {
3522+
PropName::Str(_str) => true,
3523+
_ => false
3524+
}
3525+
}
3526+
3527+
3528+
fn get_string_literal_text(string_value: String, is_jsx_attribute: bool, should_remove_quotes_if_identifier: bool, context: &mut Context) -> String {
3529+
if should_remove_quotes_if_identifier && is_valid_identifier(&string_value) {
3530+
return string_value
3531+
}
34933532
return if is_jsx_attribute {
34943533
// JSX attributes cannot contain escaped quotes so regardless of
34953534
// configuration, allow changing the quote style to single or
@@ -3507,6 +3546,16 @@ fn gen_string_literal<'a>(node: &'a Str, context: &mut Context<'a>) -> PrintItem
35073546
}
35083547
};
35093548

3549+
fn is_valid_identifier(string_value: &str) -> bool {
3550+
if string_value.len() == 0 { return false }
3551+
for (i, c) in string_value.chars().enumerate() {
3552+
if (i == 0 && !c.is_ident_start()) || !c.is_ident_part() {
3553+
return false
3554+
}
3555+
};
3556+
true
3557+
}
3558+
35103559
fn handle_prefer_double(string_value: String) -> String {
35113560
if double_to_single(&string_value) <= 0 {
35123561
format_with_double(string_value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
~~ quoteProps: asNeeded ~~
2+
== should preserve quotes when necessary ==
3+
const objectLiteral = {
4+
"foo bar": true,
5+
"foo\nbar": true,
6+
"1foo": true,
7+
"💩": true,
8+
"a💩": true,
9+
"c\d": true,
10+
"1": true,
11+
"1foo"() {},
12+
get "2foo"() {},
13+
set "2foo"(v) {},
14+
async "3foo"() {},
15+
};
16+
class Class {
17+
"1foo"() {}
18+
get "1foo"() {}
19+
set "1foo"() {}
20+
async "3foo"() {}
21+
"4foo": string;
22+
"1" = true;
23+
}
24+
interface Interface {
25+
"1foo": string;
26+
"2foo"(): string;
27+
get "3foo"(): string;
28+
set "3foo"(v: string);
29+
}
30+
type Type = {
31+
"1foo": string;
32+
"2foo"(): string;
33+
get "3foo"(): string;
34+
set "3foo"(v: string);
35+
};
36+
37+
[expect]
38+
const objectLiteral = {
39+
"foo bar": true,
40+
"foo\nbar": true,
41+
"1foo": true,
42+
"💩": true,
43+
"a💩": true,
44+
"c\d": true,
45+
"1": true,
46+
"1foo"() {},
47+
get "2foo"() {},
48+
set "2foo"(v) {},
49+
async "3foo"() {},
50+
};
51+
class Class {
52+
"1foo"() {}
53+
get "1foo"() {}
54+
set "1foo"() {}
55+
async "3foo"() {}
56+
"4foo": string;
57+
"1" = true;
58+
}
59+
interface Interface {
60+
"1foo": string;
61+
"2foo"(): string;
62+
get "3foo"(): string;
63+
set "3foo"(v: string);
64+
}
65+
type Type = {
66+
"1foo": string;
67+
"2foo"(): string;
68+
get "3foo"(): string;
69+
set "3foo"(v: string);
70+
};
71+
72+
== should remove quotes when unnecessary ==
73+
const objectLiteral = {
74+
"foo": true,
75+
"$": true,
76+
"_": true,
77+
"_1": true,
78+
'foo': true,
79+
'$': true,
80+
'_': true,
81+
'_1': true,
82+
'é': true,
83+
"foo"() {},
84+
get "foo2"() {},
85+
set "foo2"(v) {},
86+
async "foo3"() {},
87+
};
88+
class Class {
89+
"foo"() {}
90+
get "foo2"() {}
91+
set "foo2"() {}
92+
async "foo3"() {}
93+
"foo4": string;
94+
}
95+
interface Interface {
96+
"foo": string;
97+
"foo2"(): string;
98+
get "foo3"(): string;
99+
set "foo3"(v: string);
100+
}
101+
type Type = {
102+
"foo": string;
103+
"foo2"(): string;
104+
get "foo3"(): string;
105+
set "foo3"(v: string);
106+
};
107+
108+
[expect]
109+
const objectLiteral = {
110+
foo: true,
111+
$: true,
112+
_: true,
113+
_1: true,
114+
foo: true,
115+
$: true,
116+
_: true,
117+
_1: true,
118+
é: true,
119+
foo() {},
120+
get foo2() {},
121+
set foo2(v) {},
122+
async foo3() {},
123+
};
124+
class Class {
125+
foo() {}
126+
get foo2() {}
127+
set foo2() {}
128+
async foo3() {}
129+
"foo4": string;
130+
}
131+
interface Interface {
132+
foo: string;
133+
foo2(): string;
134+
get foo3(): string;
135+
set foo3(v: string);
136+
}
137+
type Type = {
138+
foo: string;
139+
foo2(): string;
140+
get foo3(): string;
141+
set foo3(v: string);
142+
};

0 commit comments

Comments
 (0)