Repository: google/starlark-go
Pull request #279 (2020-06-15 14:30:05)

syntax: permit \" and \' escapes in both "..." and '...' literals
Clarify spec on this issue.

Fixes https://github.com/google/starlark-go/issues/278

Also, fix semantic merge conflict in testdata/json.star tests that
relied on "\u" being interpreted as "\\u", causing tests to fail.


alandonovan: @laurentlb @nicks 

Patch:
diff --git a/doc/spec.md b/doc/spec.md
index c147656..7c2e66a 100644
--- a/doc/spec.md
+++ b/doc/spec.md
@@ -345,6 +345,12 @@ using the other kind, as in these examples:
 'Yes, it\'s a classic.'
 ```
 
+Literal occurrences of the _opposite_ kind of quotation mark, such as
+an apostrophe within a double-quoted string literal, may be escaped
+by a backslash, but this is not necessary: `"it's"` and `"it\'s"` are
+equivalent.
+
+
 #### String escapes
 
 Within a string literal, the backslash character `\` indicates the
diff --git a/syntax/quote.go b/syntax/quote.go
index 45c0c76..49cb259 100644
--- a/syntax/quote.go
+++ b/syntax/quote.go
@@ -130,9 +130,10 @@ func unquote(quoted string) (s string, triple bool, err error) {
 			// Ignore the escape and the line break.
 			quoted = quoted[2:]
 
-		case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote:
+		case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '\'', '"':
 			// One-char escape.
-			// We escape only the kind of quotation mark in use.
+			// Escapes are allowed for both kinds of quotation
+			// mark, not just the kind in use.
 			buf.WriteByte(unesc[quoted[1]])
 			quoted = quoted[2:]
 


Test patch:
diff --git a/starlark/testdata/json.star b/starlark/testdata/json.star
index d8af66b..615d70b 100644
--- a/starlark/testdata/json.star
+++ b/starlark/testdata/json.star
@@ -56,9 +56,9 @@ assert.eq(json.decode('[]'), [])
 assert.eq(json.decode('[1]'), [1])
 assert.eq(json.decode('[1,2,3]'), [1, 2, 3])
 assert.eq(json.decode('{"one": 1, "two": 2}'), dict(one=1, two=2))
-assert.eq(json.decode('{"foo\u0000bar": 42}'), {"foo\x00bar": 42})
-assert.eq(json.decode('"\ud83d\ude39\ud83d\udc8d"'), "😹💍")
-assert.eq(json.decode('"\u0123"'), 'ģ')
+assert.eq(json.decode('{"foo\\u0000bar": 42}'), {"foo\x00bar": 42})
+assert.eq(json.decode('"\\ud83d\\ude39\\ud83d\\udc8d"'), "😹💍")
+assert.eq(json.decode('"\\u0123"'), 'ģ')
 assert.eq(json.decode('"\x7f"'), "\x7f")
 
 def decode_error(expr, error):
@@ -68,7 +68,7 @@ decode_error('truefalse',
              "json.decode: at offset 4, unexpected character 'f' after value")
 
 decode_error('"abc', "unclosed string literal")
-decode_error('"ab\gc"', "invalid character 'g' in string escape code")
+decode_error('"ab\\gc"', "invalid character 'g' in string escape code")
 decode_error("'abc'", "unexpected character '\\\\''")
 
 decode_error("1.2.3", "invalid number: 1.2.3")
diff --git a/syntax/scan_test.go b/syntax/scan_test.go
index fecf26f..0f2d9f2 100644
--- a/syntax/scan_test.go
+++ b/syntax/scan_test.go
@@ -200,16 +200,16 @@ pass`, "pass newline pass EOF"}, // consecutive newlines are consolidated
 		{`"\+"`, `foo.star:1:1: invalid escape sequence \+`},
 		{`"\w"`, `foo.star:1:1: invalid escape sequence \w`},
 		{`"\""`, `"\"" EOF`},
-		{`"\'"`, `foo.star:1:1: invalid escape sequence \'`},
+		{`"\'"`, `"'" EOF`},
 		{`'\w'`, `foo.star:1:1: invalid escape sequence \w`},
 		{`'\''`, `"'" EOF`},
-		{`'\"'`, `foo.star:1:1: invalid escape sequence \"`},
+		{`'\"'`, `"\"" EOF`},
 		{`"""\w"""`, `foo.star:1:1: invalid escape sequence \w`},
 		{`"""\""""`, `"\"" EOF`},
-		{`"""\'"""`, `foo.star:1:1: invalid escape sequence \'`},
+		{`"""\'"""`, `"'" EOF`},
 		{`'''\w'''`, `foo.star:1:1: invalid escape sequence \w`},
 		{`'''\''''`, `"'" EOF`},
-		{`'''\"'''`, `foo.star:1:1: invalid escape sequence \"`}, // error
+		{`'''\"'''`, `"\"" EOF`},
 		{`r"\w"`, `"\\w" EOF`},
 		{`r"\""`, `"\\\"" EOF`},
 		{`r"\'"`, `"\\'" EOF`},