diff options
Diffstat (limited to 'Userland/Libraries/LibJS')
-rw-r--r-- | Userland/Libraries/LibJS/AST.cpp | 5 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Parser.cpp | 30 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Parser.h | 9 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/tagged-template-literals.js | 25 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/template-literals.js | 6 |
5 files changed, 67 insertions, 8 deletions
diff --git a/Userland/Libraries/LibJS/AST.cpp b/Userland/Libraries/LibJS/AST.cpp index c6f1fdd106..94e272af9e 100644 --- a/Userland/Libraries/LibJS/AST.cpp +++ b/Userland/Libraries/LibJS/AST.cpp @@ -3545,6 +3545,11 @@ Completion TaggedTemplateLiteral::execute(Interpreter& interpreter, GlobalObject // tag`${foo}` -> "", foo, "" -> tag(["", ""], foo) // tag`foo${bar}baz${qux}` -> "foo", bar, "baz", qux, "" -> tag(["foo", "baz", ""], bar, qux) if (i % 2 == 0) { + // If the string contains invalid escapes we get a null expression here, which we then convert + // to the expected `undefined` TV. + if (value.is_nullish()) + value = js_undefined(); + strings->indexed_properties().append(value); } else { arguments.append(value); diff --git a/Userland/Libraries/LibJS/Parser.cpp b/Userland/Libraries/LibJS/Parser.cpp index 30ee0cc961..7dcc2bd71f 100644 --- a/Userland/Libraries/LibJS/Parser.cpp +++ b/Userland/Libraries/LibJS/Parser.cpp @@ -1811,16 +1811,18 @@ NonnullRefPtr<ArrayExpression> Parser::parse_array_expression() return create_ast_node<ArrayExpression>({ m_state.current_token.filename(), rule_start.position(), position() }, move(elements)); } -NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token const& token, bool in_template_literal) +NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token const& token, StringLiteralType string_literal_type, bool* contains_invalid_escape) { auto rule_start = push_start(); auto status = Token::StringValueStatus::Ok; auto string = token.string_value(status); + // NOTE: Tagged templates should not fail on invalid strings as their raw contents can still be accessed. if (status != Token::StringValueStatus::Ok) { String message; if (status == Token::StringValueStatus::LegacyOctalEscapeSequence) { m_state.string_legacy_octal_escape_sequence_in_scope = true; - if (in_template_literal) + // It is a Syntax Error if the [Tagged] parameter was not set and Template{Head, Middle, Tail} Contains NotEscapeSequence. + if (string_literal_type != StringLiteralType::Normal) message = "Octal escape sequence not allowed in template literal"; else if (m_state.strict_mode) message = "Octal escape sequence in string literal not allowed in strict mode"; @@ -1833,11 +1835,17 @@ NonnullRefPtr<StringLiteral> Parser::parse_string_literal(Token const& token, bo VERIFY_NOT_REACHED(); } - if (!message.is_empty()) - syntax_error(message, Position { token.line_number(), token.line_column() }); + if (!message.is_empty()) { + if (contains_invalid_escape != nullptr) { + VERIFY(string_literal_type == StringLiteralType::TaggedTemplate); + *contains_invalid_escape = true; + } else { + syntax_error(message, Position { token.line_number(), token.line_column() }); + } + } } - auto is_use_strict_directive = !in_template_literal && (token.value() == "'use strict'" || token.value() == "\"use strict\""); + auto is_use_strict_directive = string_literal_type == StringLiteralType::Normal && (token.value() == "'use strict'" || token.value() == "\"use strict\""); return create_ast_node<StringLiteral>({ m_state.current_token.filename(), rule_start.position(), position() }, string, is_use_strict_directive); } @@ -1863,7 +1871,15 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal(bool is_tagged) while (!done() && !match(TokenType::TemplateLiteralEnd) && !match(TokenType::UnterminatedTemplateLiteral)) { if (match(TokenType::TemplateLiteralString)) { auto token = consume(); - expressions.append(parse_string_literal(token, true)); + bool contains_invalid_escape = false; + auto parsed_string_value = parse_string_literal(token, + is_tagged ? StringLiteralType::TaggedTemplate : StringLiteralType::NonTaggedTemplate, + is_tagged ? &contains_invalid_escape : nullptr); + // An invalid string leads to a cooked value of `undefined` but still gives the raw string. + if (contains_invalid_escape) + expressions.append(create_ast_node<NullLiteral>({ m_state.current_token.filename(), rule_start.position(), position() })); + else + expressions.append(move(parsed_string_value)); if (is_tagged) raw_strings.append(create_ast_node<StringLiteral>({ m_state.current_token.filename(), rule_start.position(), position() }, token.raw_template_value())); } else if (match(TokenType::TemplateLiteralExprStart)) { @@ -4011,7 +4027,7 @@ FlyString Parser::consume_string_value() { VERIFY(match(TokenType::StringLiteral)); auto string_token = consume(); - FlyString value = parse_string_literal(string_token, false)->value(); + FlyString value = parse_string_literal(string_token)->value(); // This also checks IsStringWellFormedUnicode which makes sure there is no unpaired surrogate // Surrogates are at least 3 bytes diff --git a/Userland/Libraries/LibJS/Parser.h b/Userland/Libraries/LibJS/Parser.h index ffbe199e9c..0fc673cee8 100644 --- a/Userland/Libraries/LibJS/Parser.h +++ b/Userland/Libraries/LibJS/Parser.h @@ -142,7 +142,14 @@ public: NonnullRefPtr<RegExpLiteral> parse_regexp_literal(); NonnullRefPtr<ObjectExpression> parse_object_expression(); NonnullRefPtr<ArrayExpression> parse_array_expression(); - NonnullRefPtr<StringLiteral> parse_string_literal(Token const& token, bool in_template_literal = false); + + enum class StringLiteralType { + Normal, + NonTaggedTemplate, + TaggedTemplate + }; + + NonnullRefPtr<StringLiteral> parse_string_literal(Token const& token, StringLiteralType string_literal_type = StringLiteralType::Normal, bool* contains_invalid_escape = nullptr); NonnullRefPtr<TemplateLiteral> parse_template_literal(bool is_tagged); ExpressionResult parse_secondary_expression(NonnullRefPtr<Expression>, int min_precedence, Associativity associate = Associativity::Right, ForbiddenTokens forbidden = {}); NonnullRefPtr<Expression> parse_call_expression(NonnullRefPtr<Expression>); diff --git a/Userland/Libraries/LibJS/Tests/tagged-template-literals.js b/Userland/Libraries/LibJS/Tests/tagged-template-literals.js index 8372333ff6..94ecc5398d 100644 --- a/Userland/Libraries/LibJS/Tests/tagged-template-literals.js +++ b/Userland/Libraries/LibJS/Tests/tagged-template-literals.js @@ -105,4 +105,29 @@ describe("tagged template literal functionality", () => { expect(raw[1]).toHaveLength(5); expect(raw[1]).toBe("\\nbar"); }); + + test("invalid escapes give undefined cooked values but can be accesed in raw form", () => { + let calls = 0; + let lastValue = null; + function noCookedButRaw(values) { + ++calls; + expect(values).not.toBeNull(); + expect(values.raw).toHaveLength(1); + expect(values.raw[0].length).toBeGreaterThan(0); + expect(values.raw[0].charAt(0)).toBe("\\"); + expect(values[0]).toBeUndefined(); + lastValue = values.raw[0]; + } + noCookedButRaw`\u`; + expect(calls).toBe(1); + expect(lastValue).toBe("\\u"); + + noCookedButRaw`\01`; + expect(calls).toBe(2); + expect(lastValue).toBe("\\01"); + + noCookedButRaw`\u{10FFFFF}`; + expect(calls).toBe(3); + expect(lastValue).toBe("\\u{10FFFFF}"); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/template-literals.js b/Userland/Libraries/LibJS/Tests/template-literals.js index 07d81509a8..985be79fa1 100644 --- a/Userland/Libraries/LibJS/Tests/template-literals.js +++ b/Userland/Libraries/LibJS/Tests/template-literals.js @@ -63,3 +63,9 @@ test("line continuation in literals (not characters)", () => { test("reference error from expressions", () => { expect(() => `${b}`).toThrowWithMessage(ReferenceError, "'b' is not defined"); }); + +test("invalid escapes should give syntax error", () => { + expect("`\\u`").not.toEval(); + expect("`\\01`").not.toEval(); + expect("`\\u{10FFFFF}`").not.toEval(); +}); |