Repository: casey/just
Pull request #2637 (2025-02-23 12:28:55)

Improve unknown start of token error message for invisible characters
Resolves https://github.com/casey/just/issues/1016

The approach I implemented is displaying the unknown token in single quotes, but if it's not an ASCII character then it is also escaped and displayed in parenthesis.

## Invisible character

### Before

```
error: Unknown start of token:
  |
1 | ﻿test:
  | ^
```

### After

```
error: Unknown start of token '' (\u{200b}):
  |
1 | ﻿test:
  | ^
```

## ASCII character

### Before

```
error: Unknown start of token:
  |
1 | %﻿test:
  | ^
```

### After

```
error: Unknown start of token '%':
  |
1 | %﻿test:
  | ^
```


Patch:
diff --git a/src/compile_error.rs b/src/compile_error.rs
index 8e50b3ee..2bc77361 100644
--- a/src/compile_error.rs
+++ b/src/compile_error.rs
@@ -291,7 +291,13 @@ impl Display for CompileError<'_> {
       }
       UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"),
       UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"),
-      UnknownStartOfToken => write!(f, "Unknown start of token:"),
+      UnknownStartOfToken { start } => {
+        write!(f, "Unknown start of token '{start}'")?;
+        if !start.is_ascii_graphic() {
+          write!(f, " (U+{:04X})", *start as u32)?;
+        }
+        Ok(())
+      }
       UnpairedCarriageReturn => write!(f, "Unpaired carriage return"),
       UnterminatedBacktick => write!(f, "Unterminated backtick"),
       UnterminatedInterpolation => write!(f, "Unterminated interpolation"),
diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs
index ca6f6fa7..0de1da29 100644
--- a/src/compile_error_kind.rs
+++ b/src/compile_error_kind.rs
@@ -153,7 +153,9 @@ pub(crate) enum CompileErrorKind<'src> {
   UnknownSetting {
     setting: &'src str,
   },
-  UnknownStartOfToken,
+  UnknownStartOfToken {
+    start: char,
+  },
   UnpairedCarriageReturn,
   UnterminatedBacktick,
   UnterminatedInterpolation,
diff --git a/src/lexer.rs b/src/lexer.rs
index ea77f818..3efdad2b 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -505,7 +505,7 @@ impl<'src> Lexer<'src> {
       _ if Self::is_identifier_start(start) => self.lex_identifier(),
       _ => {
         self.advance()?;
-        Err(self.error(UnknownStartOfToken))
+        Err(self.error(UnknownStartOfToken { start }))
       }
     }
   }
@@ -2121,7 +2121,7 @@ mod tests {
     line:   0,
     column: 0,
     width:  1,
-    kind:   UnknownStartOfToken,
+    kind:   UnknownStartOfToken { start: '%'},
   }
 
   error! {
@@ -2182,7 +2182,7 @@ mod tests {
     line:   0,
     column: 0,
     width:  1,
-    kind:   UnknownStartOfToken,
+    kind:   UnknownStartOfToken{ start: '-'},
   }
 
   error! {
@@ -2192,7 +2192,7 @@ mod tests {
     line:   0,
     column: 0,
     width:  1,
-    kind:   UnknownStartOfToken,
+    kind:   UnknownStartOfToken { start: '0' },
   }
 
   error! {
@@ -2262,7 +2262,7 @@ mod tests {
     line:   0,
     column: 1,
     width:  1,
-    kind:   UnknownStartOfToken,
+    kind:   UnknownStartOfToken { start: '%'},
   }
 
   error! {


Test patch:
diff --git a/tests/misc.rs b/tests/misc.rs
index 79b609f3..59db1e29 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -558,9 +558,11 @@ echo `echo command interpolation`
 ",
 }
 
-test! {
-  name:     line_error_spacing,
-  justfile: r"
+#[test]
+fn line_error_spacing() {
+  Test::new()
+    .justfile(
+      r"
 
 
 
@@ -572,14 +574,17 @@ test! {
 
 ^^^
 ",
-  stdout:   "",
-  stderr:   "error: Unknown start of token:
+    )
+    .stderr(
+      "error: Unknown start of token '^'
   ——▶ justfile:10:1
    │
 10 │ ^^^
    │ ^
 ",
-  status:   EXIT_FAILURE,
+    )
+    .status(EXIT_FAILURE)
+    .run();
 }
 
 test! {
@@ -1664,19 +1669,67 @@ test! {
   status:   EXIT_FAILURE,
 }
 
-test! {
-  name:     unknown_start_of_token,
-  justfile: "
+#[test]
+fn unknown_start_of_token() {
+  Test::new()
+    .justfile(
+      "
 assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
-",
-  stderr:   r"
-    error: Unknown start of token:
+      ",
+    )
+    .stderr(
+      r"
+    error: Unknown start of token '%'
      ——▶ justfile:1:25
       │
     1 │ assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
       │                         ^
   ",
-   status:   EXIT_FAILURE,
+    )
+    .status(EXIT_FAILURE)
+    .run();
+}
+
+#[test]
+fn unknown_start_of_token_invisible_unicode() {
+  Test::new()
+    .justfile(
+      "
+\u{200b}foo := 'bar'
+      ",
+    )
+    .stderr(
+      "
+error: Unknown start of token '\u{200b}' (U+200B)
+ ——▶ justfile:1:1
+  │
+1 │ \u{200b}foo := 'bar'
+  │ ^
+",
+    )
+    .status(EXIT_FAILURE)
+    .run();
+}
+
+#[test]
+fn unknown_start_of_token_ascii_control_char() {
+  Test::new()
+    .justfile(
+      "
+\0foo := 'bar'
+",
+    )
+    .stderr(
+      "
+error: Unknown start of token '\0' (U+0000)
+ ——▶ justfile:1:1
+  │
+1 │ \0foo := 'bar'
+  │ ^
+",
+    )
+    .status(EXIT_FAILURE)
+    .run();
 }
 
 test! {