Allow newlines in interpolations and } to abut interpolation }} (#2992)

This commit is contained in:
Casey Rodarmor 2025-12-06 15:39:30 -08:00 committed by GitHub
parent c85d9916cd
commit ea349d8ed0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 175 additions and 52 deletions

View file

@ -2418,9 +2418,6 @@ bar foo:
echo {{ if foo == "bar" { "hello" } else { "goodbye" } }}
```
Note the space after the final `}`! Without the space, the interpolation will
be prematurely closed.
Multiple conditionals can be chained:
```just

View file

@ -351,6 +351,18 @@ impl<'src> Lexer<'src> {
let whitespace = &self.rest()[..nonblank_index];
if self.open_delimiters_or_interpolation() {
if !whitespace.is_empty() {
while self.next_is_whitespace() {
self.advance()?;
}
self.token(Whitespace);
}
return Ok(());
}
let body_whitespace = &whitespace[..whitespace
.char_indices()
.take(self.indentation().chars().count())
@ -454,15 +466,11 @@ impl<'src> Lexer<'src> {
self.advance()?;
}
if self.open_delimiters() {
self.token(Whitespace);
} else {
let indentation = self.lexeme();
self.indentation.push(indentation);
self.token(Indent);
if self.recipe_body_pending {
self.recipe_body = true;
}
let indentation = self.lexeme();
self.indentation.push(indentation);
self.token(Indent);
if self.recipe_body_pending {
self.recipe_body = true;
}
Ok(())
@ -530,18 +538,17 @@ impl<'src> Lexer<'src> {
interpolation_start: Token<'src>,
start: char,
) -> CompileResult<'src> {
if self.rest_starts_with("}}") {
if self.rest_starts_with("}}") && self.open_delimiters.is_empty() {
// end current interpolation
if self.interpolation_stack.pop().is_none() {
self.advance()?;
self.advance()?;
self.presume_str("}}")?;
return Err(self.internal_error(
"Lexer::lex_interpolation found `}}` but was called with empty interpolation stack.",
));
}
// Emit interpolation end token
self.lex_double(InterpolationEnd)
} else if self.at_eol_or_eof() {
} else if self.at_eof() && self.open_delimiters.is_empty() {
// Return unterminated interpolation error that highlights the opening
// {{
Err(Self::unterminated_interpolation_error(interpolation_start))
@ -712,8 +719,8 @@ impl<'src> Lexer<'src> {
}
/// Return true if there are any unclosed delimiters
fn open_delimiters(&self) -> bool {
!self.open_delimiters.is_empty()
fn open_delimiters_or_interpolation(&self) -> bool {
!self.open_delimiters.is_empty() || !self.interpolation_stack.is_empty()
}
/// Lex a two-character digraph
@ -794,9 +801,8 @@ impl<'src> Lexer<'src> {
self.presume('\n')?;
}
// Emit an eol if there are no open delimiters, otherwise emit a whitespace
// token.
if self.open_delimiters() {
// Emit eol if there are no open delimiters, otherwise emit whitespace.
if self.open_delimiters_or_interpolation() {
self.token(Whitespace);
} else {
self.token(Eol);
@ -1085,6 +1091,7 @@ mod tests {
};
}
#[track_caller]
fn error(
src: &str,
offset: usize,
@ -2426,6 +2433,20 @@ mod tests {
},
}
error! {
name: unclosed_parenthesis_in_interpolation,
input: "a:\n echo {{foo(}}",
offset: 15,
line: 1,
column: 12,
width: 0,
kind: MismatchedClosingDelimiter {
close: Delimiter::Brace,
open: Delimiter::Paren,
open_line: 1,
},
}
#[test]
fn presume_error() {
let compile_error = Lexer::new("justfile".as_ref(), "!")

View file

@ -1411,6 +1411,7 @@ mod tests {
};
}
#[track_caller]
fn error(
src: &str,
offset: usize,
@ -2720,26 +2721,6 @@ mod tests {
},
}
error! {
name: unclosed_parenthesis_in_interpolation,
input: "a:\n echo {{foo(}}",
offset: 15,
line: 1,
column: 12,
width: 2,
kind: UnexpectedToken{
expected: vec![
Backtick,
Identifier,
ParenL,
ParenR,
Slash,
StringToken,
],
found: InterpolationEnd,
},
}
error! {
name: plus_following_parameter,
input: "a b c+:",

View file

@ -113,7 +113,7 @@ fn dependency_continuation() {
}
#[test]
fn no_interpolation_continuation() {
fn interpolation_continuation() {
Test::new()
.justfile(
"
@ -122,15 +122,7 @@ fn no_interpolation_continuation() {
'a' + 'b')}}
",
)
.stderr(
"
error: Unterminated interpolation
justfile:2:8
2 echo {{ (
^^
",
)
.status(EXIT_FAILURE)
.stderr("echo ab\n")
.stdout("ab\n")
.run();
}

131
tests/interpolation.rs Normal file
View file

@ -0,0 +1,131 @@
use super::*;
#[test]
fn closing_curly_brace_can_abut_interpolation_close() {
Test::new()
.justfile(
"
foo:
echo {{if 'a' == 'b' { 'c' } else { 'd' }}}
",
)
.stderr("echo d\n")
.stdout("d\n")
.run();
}
#[test]
fn eol_with_continuation_in_interpolation() {
Test::new()
.justfile(
"
foo:
echo {{(
'a'
)}}
",
)
.stderr("echo a\n")
.stdout("a\n")
.run();
}
#[test]
fn eol_without_continuation_in_interpolation() {
Test::new()
.justfile(
"
foo:
echo {{
'a'
}}
",
)
.stderr("echo a\n")
.stdout("a\n")
.run();
}
#[test]
fn comment_in_interopolation() {
Test::new()
.justfile(
"
foo:
echo {{ # hello
'a'
}}
",
)
.stderr(
"
error: Expected backtick, identifier, '(', '/', or string, but found comment
justfile:2:11
2 echo {{ # hello
^^^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn indent_and_dedent_are_ignored_in_interpolation() {
Test::new()
.justfile(
"
foo:
echo {{
'a'
+ 'b'
+ 'c'
}}
echo foo
",
)
.stderr("echo abc\necho foo\n")
.stdout("abc\nfoo\n")
.run();
}
#[test]
fn shebang_line_numbers_are_correct_with_multi_line_interpolations() {
Test::new()
.justfile(
"
foo:
#!/usr/bin/env cat
echo {{
'a'
+ 'b'
+ 'c'
}}
echo foo
",
)
.stdout(if cfg!(windows) {
"
echo abc
echo foo
"
} else {
"
#!/usr/bin/env cat
echo abc
echo foo
"
})
.run();
}

View file

@ -83,6 +83,7 @@ mod groups;
mod ignore_comments;
mod imports;
mod init;
mod interpolation;
mod invocation_directory;
mod json;
mod line_prefixes;