Skip to content

Commit 8df9605

Browse files
committed
fix(lint): exclude GitHub Actions expressions from noTemplateCurlyInString
GitHub Actions uses `${{ ... }}` syntax (double curly braces) which was incorrectly flagged as a template literal placeholder. This fix detects and skips patterns that have double opening braces followed by double closing braces, as these are GitHub Actions expressions, not JavaScript template literal mistakes.
1 parent 1550e73 commit 8df9605

File tree

3 files changed

+73
-9
lines changed

3 files changed

+73
-9
lines changed

crates/biome_js_analyze/src/lint/suspicious/no_template_curly_in_string.rs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ declare_lint_rule! {
4040
/// const a = templateFunction`Hello ${name}`;
4141
/// ```
4242
///
43+
/// GitHub Actions expressions using double curly braces are also valid:
44+
///
45+
/// ```js
46+
/// const a = "${{ inputs.abc }}";
47+
/// ```
48+
///
4349
pub NoTemplateCurlyInString {
4450
version: "1.9.3",
4551
name: "noTemplateCurlyInString",
@@ -61,16 +67,50 @@ impl Rule for NoTemplateCurlyInString {
6167
let token = node.value_token().ok()?;
6268
let text = token.text_trimmed();
6369

64-
let mut byte_iter = text.bytes().enumerate();
65-
while let Some((i, byte)) = byte_iter.next() {
66-
if byte == b'$'
67-
&& let Some((_, b'{')) = byte_iter.next()
68-
{
69-
for (j, inner_byte) in byte_iter.by_ref() {
70-
if inner_byte == b'}' {
71-
return Some((i as u32, (j + 1) as u32));
70+
let mut iter = text.bytes().enumerate().peekable();
71+
72+
while let Some((i, byte)) = iter.next() {
73+
if byte != b'$' {
74+
continue;
75+
}
76+
if iter.next_if(|(_, b)| *b == b'{').is_none() {
77+
continue;
78+
}
79+
80+
// Check for GitHub Actions syntax: ${{ ... }}
81+
if iter.next_if(|(_, b)| *b == b'{').is_some() {
82+
// Scan for closing }} sequence, tracking first } position
83+
let mut first_close_pos = None;
84+
let mut prev_was_close = false;
85+
let mut found_double_close = false;
86+
for (j, b) in iter.by_ref() {
87+
if b == b'}' {
88+
if prev_was_close {
89+
// Found }}, valid GitHub Actions expression
90+
found_double_close = true;
91+
break;
92+
}
93+
first_close_pos.get_or_insert(j);
94+
prev_was_close = true;
95+
} else {
96+
prev_was_close = false;
7297
}
7398
}
99+
if found_double_close {
100+
continue;
101+
}
102+
// No }} found - if we saw any }, flag as regular template
103+
if let Some(j) = first_close_pos {
104+
return Some((i as u32, (j + 1) as u32));
105+
}
106+
return None;
107+
}
108+
109+
// Regular ${...} pattern - find closing brace
110+
for (j, inner_byte) in iter.by_ref() {
111+
if inner_byte == b'}' {
112+
return Some((i as u32, (j + 1) as u32));
113+
}
74114
}
75115
}
76116
None

crates/biome_js_analyze/tests/specs/suspicious/noTemplateCurlyInString/valid.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,16 @@ let a = '$2';
1212
let a = '${';
1313
let a = '$}';
1414
let a = '{foo}';
15-
let a = '{foo: "bar"}';
15+
let a = '{foo: "bar"}';
16+
17+
// GitHub Actions expressions (double curly braces)
18+
let a = "${{ inputs.abc }}";
19+
let a = '${{ github.event.action }}';
20+
let a = "environment: ${{ inputs.environment }}";
21+
let a = "${{ secrets.MY_SECRET }}";
22+
23+
// GitHub Actions expressions with nested braces
24+
let a = "${{ fromJSON('{\"key\": \"value\"}') }}";
25+
let a = "${{ toJSON(github) }}";
26+
let a = "${{ format('{0} {1}', foo, bar) }}";
27+
let a = "${{ contains(github.event.head_commit.message, '[skip ci]') }}";

crates/biome_js_analyze/tests/specs/suspicious/noTemplateCurlyInString/valid.js.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,16 @@ let a = '${';
1919
let a = '$}';
2020
let a = '{foo}';
2121
let a = '{foo: "bar"}';
22+
23+
// GitHub Actions expressions (double curly braces)
24+
let a = "${{ inputs.abc }}";
25+
let a = '${{ github.event.action }}';
26+
let a = "environment: ${{ inputs.environment }}";
27+
let a = "${{ secrets.MY_SECRET }}";
28+
29+
// GitHub Actions expressions with nested braces
30+
let a = "${{ fromJSON('{\"key\": \"value\"}') }}";
31+
let a = "${{ toJSON(github) }}";
32+
let a = "${{ format('{0} {1}', foo, bar) }}";
33+
let a = "${{ contains(github.event.head_commit.message, '[skip ci]') }}";
2234
```

0 commit comments

Comments
 (0)