fix(expr): unescape every doubled quote in string literals, as the runner does
- commit
- 5f59a46a3ed5e3f88477bf109ad55f8ffaec5471
- author
- brice · · 2026-10-12 17:28 UTC
- parent
- f5fe276
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +19 -14
M expr/PORTING.md +8 -5
| @@ -64,11 +64,14 @@ Each item is implemented as the JavaScript does. Check the runner's tests | |||
| 64 | 64 | (`src/Test/L0/Expressions*`, `Sdk/Expressions2/...`) and decide for each one | |
| 65 | 65 | whether to keep the JS behaviour or follow the runner. | |
| 66 | 66 | ||
| 67 | - | - [ ] **String escapes: only the first `''` is unescaped.** The lexer does | |
| 68 | - | `value.replace("''", "'")` with a string pattern, which replaces one | |
| 69 | - | occurrence: `'a''b''c'` is `a'b''c`. The C# lexer should unescape every | |
| 70 | - | pair (`StringBuilder`/`Replace`). Probable JS bug. (`lexer.go`, | |
| 71 | - | `consumeString`; tests: `TestEvalLiteralsAndOperators`.) | |
| 67 | + | - [x] **String escapes: every `''` is unescaped.** Resolved: follows the C#. | |
| 68 | + | `LexicalAnalyzer.ReadStringToken` (`Tokens/LexicalAnalyzer.cs`, the | |
| 69 | + | "Escaped single quote" branch) skips the second quote of each pair and | |
| 70 | + | appends the first, so `'a''b''c'` is `a'b'c`. The JavaScript lexer does | |
| 71 | + | `value.replace("''", "'")` (first pair only) and gives `a'b''c`; that is | |
| 72 | + | a bug of the editor library. Tests: `TestEvalLiteralsAndOperators`; the | |
| 73 | + | oracle excuses the JS mismatch because `testdata/runner.json.gz` agrees | |
| 74 | + | with this package. | |
| 72 | 75 | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | |
| 73 | 76 | `allowCaseFunction`, but only in completion; the parser and the | |
| 74 | 77 | evaluator always accept it. Here it is always on. Confirm whether the | |
| @@ -64,11 +64,14 @@ Each item is implemented as the JavaScript does. Check the runner's tests | |||||
| 64 | (`src/Test/L0/Expressions*`, `Sdk/Expressions2/...`) and decide for each one | 64 | (`src/Test/L0/Expressions*`, `Sdk/Expressions2/...`) and decide for each one | ||
| 65 | whether to keep the JS behaviour or follow the runner. | 65 | whether to keep the JS behaviour or follow the runner. | ||
| 66 | 66 | ||||
| 67 | - | - [ ] **String escapes: only the first `''` is unescaped.** The lexer does | 67 | + | - [x] **String escapes: every `''` is unescaped.** Resolved: follows the C#. |
| 68 | - | `value.replace("''", "'")` with a string pattern, which replaces one | 68 | + | `LexicalAnalyzer.ReadStringToken` (`Tokens/LexicalAnalyzer.cs`, the |
| 69 | - | occurrence: `'a''b''c'` is `a'b''c`. The C# lexer should unescape every | 69 | + | "Escaped single quote" branch) skips the second quote of each pair and |
| 70 | - | pair (`StringBuilder`/`Replace`). Probable JS bug. (`lexer.go`, | 70 | + | appends the first, so `'a''b''c'` is `a'b'c`. The JavaScript lexer does |
| 71 | - | `consumeString`; tests: `TestEvalLiteralsAndOperators`.) | 71 | + | `value.replace("''", "'")` (first pair only) and gives `a'b''c`; that is |
| 72 | + | a bug of the editor library. Tests: `TestEvalLiteralsAndOperators`; the | |||
| 73 | + | oracle excuses the JS mismatch because `testdata/runner.json.gz` agrees | |||
| 74 | + | with this package. | |||
| 72 | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | 75 | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | ||
| 73 | `allowCaseFunction`, but only in completion; the parser and the | 76 | `allowCaseFunction`, but only in completion; the parser and the | ||
| 74 | evaluator always accept it. Here it is always on. Confirm whether the | 77 | evaluator always accept it. Here it is always on. Confirm whether the | ||
M expr/eval_test.go +4 -4
| @@ -88,11 +88,11 @@ func TestEvalLiteralsAndOperators(t *testing.T) { | |||
| 88 | 88 | {"'a'", "String:a"}, | |
| 89 | 89 | {"''", "String:"}, | |
| 90 | 90 | {"'it''s'", "String:it's"}, | |
| 91 | - | // Only the first doubled quote is unescaped: the reference's | |
| 92 | - | // value.replace("''", "'") is not global. | |
| 93 | - | {"'a''b''c'", "String:a'b''c"}, | |
| 91 | + | // Every doubled quote is unescaped, as the C# runner does (the | |
| 92 | + | // JavaScript reference stops after the first). | |
| 93 | + | {"'a''b''c'", "String:a'b'c"}, | |
| 94 | 94 | {"''''", "String:'"}, | |
| 95 | - | {"'a''''b'", "String:a'''b"}, | |
| 95 | + | {"'a''''b'", "String:a''b"}, | |
| 96 | 96 | ||
| 97 | 97 | {"1 == 1", "Boolean:true"}, | |
| 98 | 98 | {"1 == 2", "Boolean:false"}, | |
| @@ -88,11 +88,11 @@ func TestEvalLiteralsAndOperators(t *testing.T) { | |||||
| 88 | {"'a'", "String:a"}, | 88 | {"'a'", "String:a"}, | ||
| 89 | {"''", "String:"}, | 89 | {"''", "String:"}, | ||
| 90 | {"'it''s'", "String:it's"}, | 90 | {"'it''s'", "String:it's"}, | ||
| 91 | - | // Only the first doubled quote is unescaped: the reference's | 91 | + | // Every doubled quote is unescaped, as the C# runner does (the |
| 92 | - | // value.replace("''", "'") is not global. | 92 | + | // JavaScript reference stops after the first). |
| 93 | - | {"'a''b''c'", "String:a'b''c"}, | 93 | + | {"'a''b''c'", "String:a'b'c"}, |
| 94 | {"''''", "String:'"}, | 94 | {"''''", "String:'"}, | ||
| 95 | - | {"'a''''b'", "String:a'''b"}, | 95 | + | {"'a''''b'", "String:a''b"}, |
| 96 | 96 | ||||
| 97 | {"1 == 1", "Boolean:true"}, | 97 | {"1 == 1", "Boolean:true"}, | ||
| 98 | {"1 == 2", "Boolean:false"}, | 98 | {"1 == 2", "Boolean:false"}, | ||
M expr/lexer.go +7 -5
| @@ -295,11 +295,13 @@ func (l *lexer) consumeString() error { | |||
| 295 | 295 | } | |
| 296 | 296 | l.next() // closing quote | |
| 297 | 297 | value := l.substring(l.start+1, l.offset-1) | |
| 298 | - | // The reference calls value.replace("''", "'") with a string pattern, | |
| 299 | - | // which replaces only the FIRST doubled quote: 'a''b''c' is a'b''c. | |
| 300 | - | // GitHub's C# lexer unescapes every pair. Followed as written; see | |
| 301 | - | // PORTING.md. | |
| 302 | - | value = strings.Replace(value, "''", "'", 1) | |
| 298 | + | // Every doubled quote is one quote. The JavaScript reference calls | |
| 299 | + | // value.replace("''", "'") with a string pattern, which replaces only the | |
| 300 | + | // first pair ('a''b''c' is a'b''c there); the C# runner, which is what | |
| 301 | + | // runs on github.com, appends one quote per pair | |
| 302 | + | // (Tokens/LexicalAnalyzer.cs ReadStringToken, "Escaped single quote"), and | |
| 303 | + | // this package follows it. See PORTING.md. | |
| 304 | + | value = strings.ReplaceAll(value, "''", "'") | |
| 303 | 305 | l.addToken(TokenString, String(value)) | |
| 304 | 306 | return nil | |
| 305 | 307 | } | |
| @@ -295,11 +295,13 @@ func (l *lexer) consumeString() error { | |||||
| 295 | } | 295 | } | ||
| 296 | l.next() // closing quote | 296 | l.next() // closing quote | ||
| 297 | value := l.substring(l.start+1, l.offset-1) | 297 | value := l.substring(l.start+1, l.offset-1) | ||
| 298 | - | // The reference calls value.replace("''", "'") with a string pattern, | 298 | + | // Every doubled quote is one quote. The JavaScript reference calls |
| 299 | - | // which replaces only the FIRST doubled quote: 'a''b''c' is a'b''c. | 299 | + | // value.replace("''", "'") with a string pattern, which replaces only the |
| 300 | - | // GitHub's C# lexer unescapes every pair. Followed as written; see | 300 | + | // first pair ('a''b''c' is a'b''c there); the C# runner, which is what |
| 301 | - | // PORTING.md. | 301 | + | // runs on github.com, appends one quote per pair |
| 302 | - | value = strings.Replace(value, "''", "'", 1) | 302 | + | // (Tokens/LexicalAnalyzer.cs ReadStringToken, "Escaped single quote"), and |
| 303 | + | // this package follows it. See PORTING.md. | |||
| 304 | + | value = strings.ReplaceAll(value, "''", "'") | |||
| 303 | l.addToken(TokenString, String(value)) | 305 | l.addToken(TokenString, String(value)) | ||
| 304 | return nil | 306 | return nil | ||
| 305 | } | 307 | } | ||