Repository: mitsuhiko/minijinja
Pull request #626 (2024-10-31 12:47:13)

Fix a bug with parenless 'is' tests
Fixes an incompatibility with Jinja2.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1364f8c..fa81cd3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@ All notable changes to MiniJinja are documented here.
 - `minijinja-cli` now allows the template name to be set to an empty
   string when `--template` is used, to allow suppliying a data file.  #624
 - Added the missing `sameas` filter from Jinja2.  #625
+- Tests can now support one argument without parentheses like in Jinja2
+  (`1 is sameas 1`).  #626
 
 ## 2.4.0
 
diff --git a/minijinja/src/compiler/lexer.rs b/minijinja/src/compiler/lexer.rs
index 2299cb9..a06f64f 100644
--- a/minijinja/src/compiler/lexer.rs
+++ b/minijinja/src/compiler/lexer.rs
@@ -854,7 +854,6 @@ impl<'s> Tokenizer<'s> {
             Some(b'*') => Some(Token::Mul),
             Some(b'/') => Some(Token::Div),
             Some(b'%') => Some(Token::Mod),
-            Some(b'!') => Some(Token::Bang),
             Some(b'.') => Some(Token::Dot),
             Some(b',') => Some(Token::Comma),
             Some(b':') => Some(Token::Colon),
diff --git a/minijinja/src/compiler/parser.rs b/minijinja/src/compiler/parser.rs
index c498bbe..10a2446 100644
--- a/minijinja/src/compiler/parser.rs
+++ b/minijinja/src/compiler/parser.rs
@@ -460,6 +460,29 @@ impl<'a> Parser<'a> {
                         expect_token!(self, Token::Ident(name) => name, "identifier");
                     let args = if matches_token!(self, Token::ParenOpen) {
                         ok!(self.parse_args())
+                    } else if matches_token!(
+                        self,
+                        Token::Ident(_)
+                            | Token::Str(_)
+                            | Token::String(_)
+                            | Token::Int(_)
+                            | Token::Int128(_)
+                            | Token::Float(_)
+                            | Token::Plus
+                            | Token::Minus
+                            | Token::BracketOpen
+                            | Token::BraceOpen
+                    ) && !matches_token!(
+                        self,
+                        Token::Ident("and")
+                            | Token::Ident("or")
+                            | Token::Ident("else")
+                            | Token::Ident("is")
+                    ) {
+                        let span = self.stream.current_span();
+                        let mut expr = ok!(self.parse_unary_only());
+                        expr = ok!(self.parse_postfix(expr, span));
+                        vec![expr]
                     } else {
                         Vec::new()
                     };
diff --git a/minijinja/src/compiler/tokens.rs b/minijinja/src/compiler/tokens.rs
index a11033d..1dc08ac 100644
--- a/minijinja/src/compiler/tokens.rs
+++ b/minijinja/src/compiler/tokens.rs
@@ -44,8 +44,6 @@ pub enum Token<'a> {
     Pow,
     /// A mod (`%`) operator.
     Mod,
-    /// The bang (`!`) operator.
-    Bang,
     /// A dot operator (`.`)
     Dot,
     /// The comma operator (`,`)
@@ -103,7 +101,6 @@ impl<'a> fmt::Display for Token<'a> {
             Token::FloorDiv => f.write_str("`//`"),
             Token::Pow => f.write_str("`**`"),
             Token::Mod => f.write_str("`%`"),
-            Token::Bang => f.write_str("`!`"),
             Token::Dot => f.write_str("`.`"),
             Token::Comma => f.write_str("`,`"),
             Token::Colon => f.write_str("`:`"),


Test patch:
diff --git a/minijinja/src/tests.rs b/minijinja/src/tests.rs
index 088b6f7..5a37944 100644
--- a/minijinja/src/tests.rs
+++ b/minijinja/src/tests.rs
@@ -368,8 +368,8 @@ mod builtins {
     /// Checks if the value is starting with a string.
     ///
     /// ```jinja
-    /// {{ "foobar" is startingwith("foo") }} -> true
-    /// {{ "foobar" is startingwith("bar") }} -> false
+    /// {{ "foobar" is startingwith "foo" }} -> true
+    /// {{ "foobar" is startingwith "bar" }} -> false
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
     pub fn is_startingwith(v: Cow<'_, str>, other: Cow<'_, str>) -> bool {
@@ -379,8 +379,8 @@ mod builtins {
     /// Checks if the value is ending with a string.
     ///
     /// ```jinja
-    /// {{ "foobar" is endingwith("bar") }} -> true
-    /// {{ "foobar" is endingwith("foo") }} -> false
+    /// {{ "foobar" is endingwith "bar" }} -> true
+    /// {{ "foobar" is endingwith "foo" }} -> false
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
     pub fn is_endingwith(v: Cow<'_, str>, other: Cow<'_, str>) -> bool {
@@ -574,10 +574,10 @@ mod builtins {
     /// compare equal.
     ///
     /// ```jinja
-    /// {{ [1, 2, 3] is sameas([1, 2, 3]) }}
+    /// {{ [1, 2, 3] is sameas [1, 2, 3] }}
     ///     -> false
     ///
-    /// {{ false is sameas(false) }}
+    /// {{ false is sameas false }}
     ///     -> true
     /// ```
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
diff --git a/minijinja/tests/inputs/tests.txt b/minijinja/tests/inputs/tests.txt
index 725a9ee..a377351 100644
--- a/minijinja/tests/inputs/tests.txt
+++ b/minijinja/tests/inputs/tests.txt
@@ -38,7 +38,9 @@ map: {{ map is mapping }}
 string: {{ string is string }}
 not-string: {{ mapping is string }}
 starts-with-a: {{ string is startingwith('a') }}
+starts-with-a-noparen: {{ string is startingwith 'a' }}
 ends-with-ha: {{ string is endingwith('ha') }}
+ends-with-ha-noparen: {{ string is endingwith 'ha' }}
 not-safe: {{ "foo" is safe }}
 safe: {{ "foo"|escape is safe }}
 is-true: {{ true is true }} | {{ 42 is true }}
@@ -47,8 +49,13 @@ is-filter: {{ 'escape' is filter }} | {{ 'unknown-filter' is filter }}
 is-test: {{ 'safe' is test }} | {{ 'unknown-test' is test }}
 is-boolean: {{ true is boolean }} | {{ 42 is boolean }}
 is-divisibleby: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby(2) }}
+is-divisibleby-noparen: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby 2 }}
 is-lower: {{ "foo" is lower }} | {{ "FOO" is lower }}
 is-upper: {{ "foo" is upper }} | {{ "FOO" is upper }}
 seq-same-as: {{ [1, 2, 3] is sameas([1, 2, 3]) }}
+seq-same-as-noparen: {{ [1, 2, 3] is sameas [1, 2, 3] }}
 const-same-as: {{ true is sameas(true) }}
-int-same-as: {{ 1 is sameas(1.0) }}
\ No newline at end of file
+const-same-as-noparen: {{ true is sameas true }}
+int-same-as: {{ 1 is sameas(1.0) }}
+int-same-as-noparen: {{ 1 is sameas 1.0 }}
+neg-int-same-as-noparen: {{ -1 is sameas -1 }}
\ No newline at end of file
diff --git a/minijinja/tests/lexer-inputs/operators.txt b/minijinja/tests/lexer-inputs/operators.txt
index 632b5d4..019ca14 100644
--- a/minijinja/tests/lexer-inputs/operators.txt
+++ b/minijinja/tests/lexer-inputs/operators.txt
@@ -1,3 +1,3 @@
 {}
 ---
-{{ (!a + b) * (c - d) / e % f // g ~ h }}
\ No newline at end of file
+{{ (a + b) * (c - d) / e % f // g ~ h }}
\ No newline at end of file
diff --git a/minijinja/tests/snapshots/test_lexer__lexer@operators.txt.snap b/minijinja/tests/snapshots/test_lexer__lexer@operators.txt.snap
index 99e0795..8f24220 100644
--- a/minijinja/tests/snapshots/test_lexer__lexer@operators.txt.snap
+++ b/minijinja/tests/snapshots/test_lexer__lexer@operators.txt.snap
@@ -1,14 +1,12 @@
 ---
 source: minijinja/tests/test_lexer.rs
-description: "{{ (!a + b) * (c - d) / e % f // g ~ h }}"
+description: "{{ (a + b) * (c - d) / e % f // g ~ h }}"
 input_file: minijinja/tests/lexer-inputs/operators.txt
 ---
 VariableStart
   "{{"
 ParenOpen
   "("
-Bang
-  "!"
 Ident("a")
   "a"
 Plus
@@ -47,4 +45,3 @@ Ident("h")
   "h"
 VariableEnd
   "}}"
-
diff --git a/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap b/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap
index 4378c3d..295c689 100644
--- a/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@tests.txt.snap
@@ -1,6 +1,6 @@
 ---
 source: minijinja/tests/test_templates.rs
-description: "even: {{ two is even }}\nodd: {{ two is odd }}\nundefined: {{ two is undefined }}\ndefined: {{ two is defined }}\nundefined2: {{ ohwell is undefined }}\ndefined2: {{ ohwell is defined }}\nnone: {{ none is none }}\nnot-none: {{ 42 is not none }}\nnumber-int: {{ two is number }}\nnumber-float: {{ two_dot_two is number }}\ninteger-int: {{ 42 is integer }}\ninteger-float: {{ 42.0 is integer }}\nfloat-int: {{ 42 is float }}\nfloat-float: {{ 42.0 is float }}\nnot-seq: {{ two is sequence }}\nseq: {{ seq is sequence }}\nreverse-not-seq: {{ seq|reverse is sequence }}\niterable: {{ seq is iterable }}\niterable-reverse: {{ seq|reverse is iterable }}\nstring-iterable: {{ string is iterable }}\nnot-iterable: {{ two is iterable }}\nnot-map: {{ two is mapping }}\nmap: {{ map is mapping }}\nstring: {{ string is string }}\nnot-string: {{ mapping is string }}\nstarts-with-a: {{ string is startingwith('a') }}\nends-with-ha: {{ string is endingwith('ha') }}\nnot-safe: {{ \"foo\" is safe }}\nsafe: {{ \"foo\"|escape is safe }}\nis-true: {{ true is true }} | {{ 42 is true }}\nis-false: {{ false is false }} | {{ 0 is false }}\nis-filter: {{ 'escape' is filter }} | {{ 'unknown-filter' is filter }}\nis-test: {{ 'safe' is test }} | {{ 'unknown-test' is test }}\nis-boolean: {{ true is boolean }} | {{ 42 is boolean }}\nis-divisibleby: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby(2) }}\nis-lower: {{ \"foo\" is lower }} | {{ \"FOO\" is lower }}\nis-upper: {{ \"foo\" is upper }} | {{ \"FOO\" is upper }}\nseq-same-as: {{ [1, 2, 3] is sameas([1, 2, 3]) }}\nconst-same-as: {{ true is sameas(true) }}\nint-same-as: {{ 1 is sameas(1.0) }}"
+description: "even: {{ two is even }}\nodd: {{ two is odd }}\nundefined: {{ two is undefined }}\ndefined: {{ two is defined }}\nundefined2: {{ ohwell is undefined }}\ndefined2: {{ ohwell is defined }}\nnone: {{ none is none }}\nnot-none: {{ 42 is not none }}\nnumber-int: {{ two is number }}\nnumber-float: {{ two_dot_two is number }}\ninteger-int: {{ 42 is integer }}\ninteger-float: {{ 42.0 is integer }}\nfloat-int: {{ 42 is float }}\nfloat-float: {{ 42.0 is float }}\nnot-seq: {{ two is sequence }}\nseq: {{ seq is sequence }}\nreverse-not-seq: {{ seq|reverse is sequence }}\niterable: {{ seq is iterable }}\niterable-reverse: {{ seq|reverse is iterable }}\nstring-iterable: {{ string is iterable }}\nnot-iterable: {{ two is iterable }}\nnot-map: {{ two is mapping }}\nmap: {{ map is mapping }}\nstring: {{ string is string }}\nnot-string: {{ mapping is string }}\nstarts-with-a: {{ string is startingwith('a') }}\nstarts-with-a-noparen: {{ string is startingwith 'a' }}\nends-with-ha: {{ string is endingwith('ha') }}\nends-with-ha-noparen: {{ string is endingwith 'ha' }}\nnot-safe: {{ \"foo\" is safe }}\nsafe: {{ \"foo\"|escape is safe }}\nis-true: {{ true is true }} | {{ 42 is true }}\nis-false: {{ false is false }} | {{ 0 is false }}\nis-filter: {{ 'escape' is filter }} | {{ 'unknown-filter' is filter }}\nis-test: {{ 'safe' is test }} | {{ 'unknown-test' is test }}\nis-boolean: {{ true is boolean }} | {{ 42 is boolean }}\nis-divisibleby: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby(2) }}\nis-divisibleby-noparen: {{ 42 is divisibleby(2) }} | {{ 41 is divisibleby 2 }}\nis-lower: {{ \"foo\" is lower }} | {{ \"FOO\" is lower }}\nis-upper: {{ \"foo\" is upper }} | {{ \"FOO\" is upper }}\nseq-same-as: {{ [1, 2, 3] is sameas([1, 2, 3]) }}\nseq-same-as-noparen: {{ [1, 2, 3] is sameas [1, 2, 3] }}\nconst-same-as: {{ true is sameas(true) }}\nconst-same-as-noparen: {{ true is sameas true }}\nint-same-as: {{ 1 is sameas(1.0) }}\nint-same-as-noparen: {{ 1 is sameas 1.0 }}\nneg-int-same-as-noparen: {{ -1 is sameas -1 }}"
 info:
   two: 2
   two_dot_two: 2.2
@@ -39,7 +39,9 @@ map: true
 string: true
 not-string: false
 starts-with-a: true
+starts-with-a-noparen: true
 ends-with-ha: true
+ends-with-ha-noparen: true
 not-safe: false
 safe: true
 is-true: true | false
@@ -48,8 +50,13 @@ is-filter: true | false
 is-test: true | false
 is-boolean: true | false
 is-divisibleby: true | false
+is-divisibleby-noparen: true | false
 is-lower: true | false
 is-upper: false | true
 seq-same-as: false
+seq-same-as-noparen: false
 const-same-as: true
+const-same-as-noparen: true
 int-same-as: false
+int-same-as-noparen: false
+neg-int-same-as-noparen: true