Skip to content

Commit 75b6a0d

Browse files
authored
feat(linter): add rule noJsxLiterals (#7248)
1 parent 733828c commit 75b6a0d

32 files changed

Lines changed: 1310 additions & 51 deletions

.changeset/nine-turkeys-dig.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery lint rule `noJsxLiterals`, which disallows the use of string literals inside JSX.
6+
7+
The rule catches these cases:
8+
9+
```jsx
10+
<>
11+
<div>test</div> {/* test is invalid */}
12+
<>test</>
13+
<div>
14+
{/* this string is invalid */}
15+
asdjfl
16+
test
17+
foo
18+
</div>
19+
</>
20+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 71 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ define_categories! {
165165
"lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises",
166166
"lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion",
167167
"lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles",
168+
"lint/nursery/noJsxLiterals": "https://biomejs.dev/linter/rules/no-jsx-literals",
168169
"lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword",
169170
"lint/nursery/noMisusedPromises": "https://biomejs.dev/linter/rules/no-misused-promises",
170171
"lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use biome_analyze::declare_lint_group;
66
pub mod no_floating_promises;
77
pub mod no_import_cycles;
8+
pub mod no_jsx_literals;
89
pub mod no_misused_promises;
910
pub mod no_next_async_client_component;
1011
pub mod no_non_null_asserted_optional_chain;
@@ -29,4 +30,4 @@ pub mod use_qwik_classlist;
2930
pub mod use_react_function_components;
3031
pub mod use_sorted_classes;
3132
pub mod use_vue_multi_word_component_names;
32-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
33+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::{
6+
AnyJsExpression, AnyJsLiteralExpression, JsFileSource, JsStringLiteralExpression, JsxAttribute,
7+
JsxExpressionAttributeValue, JsxString, JsxText, inner_string_text,
8+
};
9+
use biome_rowan::{AstNode, AstNodeList, TextRange, declare_node_union};
10+
use biome_rule_options::no_jsx_literals::NoJsxLiteralsOptions;
11+
12+
declare_lint_rule! {
13+
/// Disallow string literals inside JSX elements.
14+
///
15+
/// This rule discourages the use of
16+
/// string literals directly within JSX elements. String literals in JSX can make code harder
17+
/// to maintain, especially in applications that require internationalization or dynamic content.
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```jsx,expect_diagnostic
24+
/// <div>Hello World</div>
25+
/// ```
26+
///
27+
/// ```jsx,expect_diagnostic
28+
/// <>Welcome to our site</>
29+
/// ```
30+
///
31+
/// ```jsx,expect_diagnostic
32+
/// <span>
33+
/// Please enter your name
34+
/// </span>
35+
/// ```
36+
///
37+
/// ### Valid
38+
///
39+
/// ```jsx
40+
/// <div>{'Hello World'}</div>
41+
/// ```
42+
///
43+
/// ```jsx
44+
/// <>{'Welcome to our site'}</>
45+
/// ```
46+
///
47+
/// ```jsx
48+
/// <span>
49+
/// {'Please enter your name'}
50+
/// </span>
51+
/// ```
52+
///
53+
/// ```jsx
54+
/// <div>{`Hello ${name}`}</div>
55+
/// ```
56+
///
57+
/// ## Options
58+
///
59+
/// ### `noStrings`
60+
///
61+
/// When enabled, the rule will also flag string literals inside JSX expressions and attributes.
62+
///
63+
/// > **Default:** `false`
64+
///
65+
/// ```json,options
66+
/// {
67+
/// "options": {
68+
/// "noStrings": true
69+
/// }
70+
/// }
71+
/// ```
72+
///
73+
/// ```jsx,expect_diagnostic,use_options
74+
/// <span>
75+
/// {'Please enter your name'}
76+
/// </span>
77+
/// ```
78+
/// ```jsx,expect_diagnostic,use_options
79+
/// <Component title="Hello!" />
80+
/// ```
81+
///
82+
///
83+
///
84+
/// ### `allowedStrings`
85+
///
86+
/// An array of strings that are allowed as literals. This can be useful for common words
87+
/// or characters that don't need to be wrapped in expressions.
88+
///
89+
/// ```json,options
90+
/// {
91+
/// "options": {
92+
/// "allowedStrings": ["Hello", "&nbsp;", "·"]
93+
/// }
94+
/// }
95+
/// ```
96+
///
97+
/// ```jsx,use_options
98+
/// <>
99+
/// <div>Hello</div>
100+
/// <div>&nbsp;</div>
101+
/// <div>·</div>
102+
/// </>
103+
/// ```
104+
///
105+
/// ### `ignoreProps`
106+
///
107+
/// When enabled, the rule will ignore string literals used as prop values.
108+
///
109+
/// > **Default:** `false`
110+
///
111+
/// ```json,options
112+
/// {
113+
/// "options": {
114+
/// "ignoreProps": true
115+
/// }
116+
/// }
117+
/// ```
118+
///
119+
/// ```jsx,use_options
120+
/// <>
121+
/// <Component title="Welcome" />
122+
/// <input placeholder="Enter name" />
123+
/// </>
124+
/// ```
125+
///
126+
pub NoJsxLiterals {
127+
version: "next",
128+
name: "noJsxLiterals",
129+
language: "jsx",
130+
recommended: false,
131+
sources: &[RuleSource::EslintReact("jsx-no-literals").same()],
132+
}
133+
}
134+
135+
impl Rule for NoJsxLiterals {
136+
type Query = Ast<AnyJsxText>;
137+
type State = TextRange;
138+
type Signals = Option<Self::State>;
139+
type Options = NoJsxLiteralsOptions;
140+
141+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
142+
let file_source = ctx.source_type::<JsFileSource>();
143+
if !file_source.is_jsx() {
144+
return None;
145+
}
146+
147+
let node = ctx.query();
148+
let options = ctx.options();
149+
150+
if options.ignore_props
151+
&& node
152+
.syntax()
153+
.ancestors()
154+
.skip(1)
155+
.any(|n| JsxAttribute::can_cast(n.kind()))
156+
{
157+
return None;
158+
}
159+
160+
let value_token = match node {
161+
AnyJsxText::JsxText(text) => text.value_token().ok()?,
162+
AnyJsxText::JsStringLiteralExpression(expression) => {
163+
if !options.no_strings {
164+
return None;
165+
}
166+
expression.value_token().ok()?
167+
}
168+
AnyJsxText::JsxString(string) => {
169+
if !options.no_strings {
170+
return None;
171+
}
172+
string.value_token().ok()?
173+
}
174+
AnyJsxText::JsxExpressionAttributeValue(expression) => {
175+
if !options.no_strings {
176+
return None;
177+
}
178+
let expression = expression.expression().ok()?;
179+
match expression {
180+
AnyJsExpression::AnyJsLiteralExpression(
181+
AnyJsLiteralExpression::JsStringLiteralExpression(string_literal),
182+
) => string_literal.value_token().ok()?,
183+
AnyJsExpression::JsTemplateExpression(expression) => {
184+
return if expression.elements().len() <= 1 {
185+
Some(expression.range())
186+
} else {
187+
None
188+
};
189+
}
190+
191+
_ => return None,
192+
}
193+
}
194+
};
195+
196+
for allowed_string in &options.allowed_strings {
197+
if inner_string_text(&value_token) == allowed_string.as_ref() {
198+
return None;
199+
}
200+
}
201+
202+
if inner_string_text(&value_token).trim().is_empty() {
203+
return None;
204+
}
205+
206+
Some(value_token.text_trimmed_range())
207+
}
208+
209+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
210+
Some(
211+
RuleDiagnostic::new(
212+
rule_category!(),
213+
state,
214+
markup! {
215+
"Incorrect use of string literal detected."
216+
},
217+
)
218+
.note(markup! {
219+
"String literals in JSX can make code harder to maintain and internationalize."
220+
})
221+
.note(markup! {
222+
"Consider avoiding hardcoded strings entirely."
223+
}),
224+
)
225+
}
226+
}
227+
228+
declare_node_union! {
229+
pub AnyJsxText = JsxText
230+
| JsStringLiteralExpression
231+
| JsxString
232+
| JsxExpressionAttributeValue
233+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// should generate diagnostics
2+
<>
3+
<div>test</div>
4+
<>test</>
5+
<>
6+
<div>
7+
asdjfl
8+
test
9+
foo
10+
</div>
11+
</>
12+
</>
13+
14+
15+
class Comp1 extends Component {
16+
render() {
17+
const varObjectTest = { testKey : (<div>test</div>) };
18+
return varObjectTest.testKey;
19+
}
20+
}

0 commit comments

Comments
 (0)