templater: add != operator as user would probably expect that it exists
Some checks are pending
binaries / Build binary artifacts (linux-aarch64-gnu, ubuntu-24.04, aarch64-unknown-linux-gnu) (push) Waiting to run
binaries / Build binary artifacts (linux-aarch64-musl, ubuntu-24.04, aarch64-unknown-linux-musl) (push) Waiting to run
binaries / Build binary artifacts (linux-x86_64-gnu, ubuntu-24.04, x86_64-unknown-linux-gnu) (push) Waiting to run
binaries / Build binary artifacts (linux-x86_64-musl, ubuntu-24.04, x86_64-unknown-linux-musl) (push) Waiting to run
binaries / Build binary artifacts (macos-aarch64, macos-14, aarch64-apple-darwin) (push) Waiting to run
binaries / Build binary artifacts (macos-x86_64, macos-13, x86_64-apple-darwin) (push) Waiting to run
binaries / Build binary artifacts (win-x86_64, windows-2022, x86_64-pc-windows-msvc) (push) Waiting to run
nix / flake check (macos-14) (push) Waiting to run
nix / flake check (ubuntu-latest) (push) Waiting to run
build / build (, macos-13) (push) Waiting to run
build / build (, macos-14) (push) Waiting to run
build / build (, ubuntu-latest) (push) Waiting to run
build / build (, windows-latest) (push) Waiting to run
build / build (--all-features, ubuntu-latest) (push) Waiting to run
build / Build jj-lib without Git support (push) Waiting to run
build / Check protos (push) Waiting to run
build / Check formatting (push) Waiting to run
build / Check that MkDocs can build the docs (push) Waiting to run
build / Check that MkDocs can build the docs with Poetry 1.8 (push) Waiting to run
build / cargo-deny (advisories) (push) Waiting to run
build / cargo-deny (bans licenses sources) (push) Waiting to run
build / Clippy check (push) Waiting to run
Codespell / Codespell (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-latest) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

This commit is contained in:
Yuya Nishihara 2024-11-10 16:23:54 +09:00
parent b556fc6093
commit 6739dccc6d
6 changed files with 46 additions and 22 deletions

View file

@ -14,8 +14,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New features
* Templates now support the `==` logical operator for `Boolean`, `Integer`, and
`String` types.
* Templates now support the `==` and `!=` logical operators for `Boolean`,
`Integer`, and `String` types.
* The `jj desc` and `jj st` aliases are now hidden to not interfere with shell
completion. They remain available.

View file

@ -41,10 +41,11 @@ concat_op = { "++" }
logical_or_op = { "||" }
logical_and_op = { "&&" }
logical_eq_op = { "==" }
logical_ne_op = { "!=" }
logical_not_op = { "!" }
negate_op = { "-" }
prefix_ops = _{ logical_not_op | negate_op }
infix_ops = _{ logical_or_op | logical_and_op | logical_eq_op }
infix_ops = _{ logical_or_op | logical_and_op | logical_eq_op | logical_ne_op }
function = { identifier ~ "(" ~ whitespace* ~ function_arguments ~ whitespace* ~ ")" }
keyword_argument = { identifier ~ whitespace* ~ "=" ~ whitespace* ~ template }

View file

@ -638,7 +638,7 @@ fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
Ok(L::wrap_boolean(out))
}
BinaryOp::LogicalEq => {
BinaryOp::LogicalEq | BinaryOp::LogicalNe => {
let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
let lty = lhs.type_name();
@ -647,7 +647,11 @@ fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
let message = format!(r#"Cannot compare expressions of type "{lty}" and "{rty}""#);
TemplateParseError::expression(message, span)
})?;
Ok(L::wrap_boolean(out))
match op {
BinaryOp::LogicalEq => Ok(L::wrap_boolean(out)),
BinaryOp::LogicalNe => Ok(L::wrap_boolean(out.map(|eq| !eq))),
_ => unreachable!(),
}
}
}
}
@ -1728,14 +1732,14 @@ mod tests {
env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r#"
insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r"
--> 1:13
|
1 | description ()
| ^---
|
= expected <EOI>, `++`, `||`, `&&`, or `==`
"#);
= expected <EOI>, `++`, `||`, `&&`, `==`, or `!=`
");
insta::assert_snapshot!(env.parse_err(r#"foo"#), @r###"
--> 1:1
@ -1787,10 +1791,10 @@ mod tests {
|
= Cannot compare expressions of type "Boolean" and "Integer"
"#);
insta::assert_snapshot!(env.parse_err(r#"true == 'a'"#), @r#"
insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @r#"
--> 1:1
|
1 | true == 'a'
1 | true != 'a'
| ^---------^
|
= Cannot compare expressions of type "Boolean" and "String"
@ -1803,10 +1807,10 @@ mod tests {
|
= Cannot compare expressions of type "Integer" and "Boolean"
"#);
insta::assert_snapshot!(env.parse_err(r#"1 == 'a'"#), @r#"
insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @r#"
--> 1:1
|
1 | 1 == 'a'
1 | 1 != 'a'
| ^------^
|
= Cannot compare expressions of type "Integer" and "String"
@ -1819,10 +1823,10 @@ mod tests {
|
= Cannot compare expressions of type "String" and "Boolean"
"#);
insta::assert_snapshot!(env.parse_err(r#"'a' == 1"#), @r#"
insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @r#"
--> 1:1
|
1 | 'a' == 1
1 | 'a' != 1
| ^------^
|
= Cannot compare expressions of type "String" and "Integer"
@ -2054,10 +2058,16 @@ mod tests {
insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");

View file

@ -75,6 +75,7 @@ impl Rule {
Rule::logical_or_op => Some("||"),
Rule::logical_and_op => Some("&&"),
Rule::logical_eq_op => Some("=="),
Rule::logical_ne_op => Some("!="),
Rule::logical_not_op => Some("!"),
Rule::negate_op => Some("-"),
Rule::prefix_ops => None,
@ -377,6 +378,8 @@ pub enum BinaryOp {
LogicalAnd,
/// `==`
LogicalEq,
/// `!=`
LogicalNe,
}
pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>;
@ -507,7 +510,8 @@ fn parse_expression_node(pair: Pair<Rule>) -> TemplateParseResult<ExpressionNode
PrattParser::new()
.op(Op::infix(Rule::logical_or_op, Assoc::Left))
.op(Op::infix(Rule::logical_and_op, Assoc::Left))
.op(Op::infix(Rule::logical_eq_op, Assoc::Left))
.op(Op::infix(Rule::logical_eq_op, Assoc::Left)
| Op::infix(Rule::logical_ne_op, Assoc::Left))
.op(Op::prefix(Rule::logical_not_op) | Op::prefix(Rule::negate_op))
});
PRATT
@ -528,6 +532,7 @@ fn parse_expression_node(pair: Pair<Rule>) -> TemplateParseResult<ExpressionNode
Rule::logical_or_op => BinaryOp::LogicalOr,
Rule::logical_and_op => BinaryOp::LogicalAnd,
Rule::logical_eq_op => BinaryOp::LogicalEq,
Rule::logical_ne_op => BinaryOp::LogicalNe,
r => panic!("unexpected infix operator rule {r:?}"),
};
let lhs = Box::new(lhs?);
@ -856,8 +861,8 @@ mod tests {
parse_normalized("(!(x.f())) || (!(g()))"),
);
assert_eq!(
parse_normalized("!x.f() == !x.f() || !g() == !g()"),
parse_normalized("((!(x.f())) == (!(x.f()))) || ((!(g())) == (!(g())))"),
parse_normalized("!x.f() == !x.f() || !g() != !g()"),
parse_normalized("((!(x.f())) == (!(x.f()))) || ((!(g())) != (!(g())))"),
);
assert_eq!(
parse_normalized("x.f() || y == y || z"),
@ -867,6 +872,10 @@ mod tests {
parse_normalized("x || y == y && z.h() == z"),
parse_normalized("x || ((y == y) && ((z.h()) == z))"),
);
assert_eq!(
parse_normalized("x == y || y != z && !z"),
parse_normalized("(x == y) || ((y != z) && (!z))"),
);
// Logical operator bounds more tightly than concatenation. This might
// not be so intuitive, but should be harmless.
@ -882,6 +891,10 @@ mod tests {
parse_normalized(r"x == y ++ z"),
parse_normalized(r"(x == y) ++ z"),
);
assert_eq!(
parse_normalized(r"x != y ++ z"),
parse_normalized(r"(x != y) ++ z"),
);
// Expression span
assert_eq!(parse_template(" ! x ").unwrap().span.as_str(), "! x");

View file

@ -25,15 +25,15 @@ fn test_templater_parse_error() {
let repo_path = test_env.env_root().join("repo");
let render_err = |template| test_env.jj_cmd_failure(&repo_path, &["log", "-T", template]);
insta::assert_snapshot!(render_err(r#"description ()"#), @r#"
insta::assert_snapshot!(render_err(r#"description ()"#), @r"
Error: Failed to parse template: Syntax error
Caused by: --> 1:13
|
1 | description ()
| ^---
|
= expected <EOI>, `++`, `||`, `&&`, or `==`
"#);
= expected <EOI>, `++`, `||`, `&&`, `==`, or `!=`
");
// Typo
test_env.add_config(

View file

@ -31,8 +31,8 @@ The following operators are supported.
* `x.f()`: Method call.
* `-x`: Negate integer value.
* `!x`: Logical not.
* `x == y`: Logical equal. Operands must be either `Boolean`, `Integer`, or
`String`.
* `x == y`, `x != y`: Logical equal/not equal. Operands must be either
`Boolean`, `Integer`, or `String`.
* `x && y`: Logical and, short-circuiting.
* `x || y`: Logical or, short-circuiting.
* `x ++ y`: Concatenate `x` and `y` templates.