summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Groh <mail@linusgroh.de>2020-05-06 10:17:35 +0100
committerAndreas Kling <kling@serenityos.org>2020-05-06 14:49:53 +0200
commit4d20cf57dbc32f470faf93b637f01432e3c65696 (patch)
tree187097d9253db5b64113fe456e48b9c8249b02b6
parenteea62dd3656e0ba87f0f573009e338e36063b62c (diff)
downloadserenity-4d20cf57dbc32f470faf93b637f01432e3c65696.zip
LibJS: Implement tagged template literals (foo`bar`)
To make processing tagged template literals easier, template literals will now add one empty StringLiteral before and after each template expression *if* there's no other string - e.g.: `${foo}` -> "", foo, "" `test${foo}${bar}test` -> "test", foo, "", bar, "test" This also matches the behaviour of many other parsers.
-rw-r--r--Libraries/LibJS/AST.cpp44
-rw-r--r--Libraries/LibJS/AST.h18
-rw-r--r--Libraries/LibJS/Parser.cpp14
-rw-r--r--Libraries/LibJS/Tests/tagged-template-literals.js88
4 files changed, 161 insertions, 3 deletions
diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp
index 77250683a0..eb643cc794 100644
--- a/Libraries/LibJS/AST.cpp
+++ b/Libraries/LibJS/AST.cpp
@@ -1277,8 +1277,7 @@ Value ArrayExpression::execute(Interpreter& interpreter) const
void TemplateLiteral::dump(int indent) const
{
ASTNode::dump(indent);
-
- for (auto& expression : expressions())
+ for (auto& expression : m_expressions)
expression.dump(indent + 1);
}
@@ -1286,7 +1285,7 @@ Value TemplateLiteral::execute(Interpreter& interpreter) const
{
StringBuilder string_builder;
- for (auto& expression : expressions()) {
+ for (auto& expression : m_expressions) {
auto expr = expression.execute(interpreter);
if (interpreter.exception())
return {};
@@ -1296,6 +1295,45 @@ Value TemplateLiteral::execute(Interpreter& interpreter) const
return js_string(interpreter, string_builder.build());
}
+void TaggedTemplateLiteral::dump(int indent) const
+{
+ ASTNode::dump(indent);
+ print_indent(indent + 1);
+ printf("(Tag)\n");
+ m_tag->dump(indent + 2);
+ print_indent(indent + 1);
+ printf("(Template Literal)\n");
+ m_template_literal->dump(indent + 2);
+}
+
+Value TaggedTemplateLiteral::execute(Interpreter& interpreter) const
+{
+ auto tag = m_tag->execute(interpreter);
+ if (interpreter.exception())
+ return {};
+ if (!tag.is_function()) {
+ interpreter.throw_exception<TypeError>(String::format("%s is not a function", tag.to_string().characters()));
+ return {};
+ }
+ auto& tag_function = tag.as_function();
+ auto& expressions = m_template_literal->expressions();
+ auto* strings = Array::create(interpreter.global_object());
+ MarkedValueList arguments(interpreter.heap());
+ arguments.append(strings);
+ for (size_t i = 0; i < expressions.size(); ++i) {
+ auto value = expressions[i].execute(interpreter);
+ if (interpreter.exception())
+ return {};
+ // tag`${foo}` -> "", foo, "" -> tag(["", ""], foo)
+ // tag`foo${bar}baz${qux}` -> "foo", bar, "baz", qux, "" -> tag(["foo", "baz", ""], bar, qux)
+ if (i % 2 == 0)
+ strings->elements().append(value);
+ else
+ arguments.append(value);
+ }
+ return interpreter.call(tag_function, js_undefined(), move(arguments));
+}
+
void TryStatement::dump(int indent) const
{
ASTNode::dump(indent);
diff --git a/Libraries/LibJS/AST.h b/Libraries/LibJS/AST.h
index 245fa7ac4d..8ab77d53fb 100644
--- a/Libraries/LibJS/AST.h
+++ b/Libraries/LibJS/AST.h
@@ -786,6 +786,24 @@ private:
const NonnullRefPtrVector<Expression> m_expressions;
};
+class TaggedTemplateLiteral final : public Expression {
+public:
+ TaggedTemplateLiteral(NonnullRefPtr<Expression> tag, NonnullRefPtr<TemplateLiteral> template_literal)
+ : m_tag(move(tag))
+ , m_template_literal(move(template_literal))
+ {
+ }
+
+ virtual Value execute(Interpreter&) const override;
+ virtual void dump(int indent) const override;
+
+private:
+ virtual const char* class_name() const override { return "TaggedTemplateLiteral"; }
+
+ const NonnullRefPtr<Expression> m_tag;
+ const NonnullRefPtr<TemplateLiteral> m_template_literal;
+};
+
class MemberExpression final : public Expression {
public:
MemberExpression(NonnullRefPtr<Expression> object, NonnullRefPtr<Expression> property, bool computed = false)
diff --git a/Libraries/LibJS/Parser.cpp b/Libraries/LibJS/Parser.cpp
index a665ec2872..7c82bd6de3 100644
--- a/Libraries/LibJS/Parser.cpp
+++ b/Libraries/LibJS/Parser.cpp
@@ -560,6 +560,9 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal()
NonnullRefPtrVector<Expression> expressions;
+ if (!match(TokenType::TemplateLiteralString))
+ expressions.append(create_ast_node<StringLiteral>(""));
+
while (!match(TokenType::TemplateLiteralEnd) && !match(TokenType::UnterminatedTemplateLiteral)) {
if (match(TokenType::TemplateLiteralString)) {
expressions.append(create_ast_node<StringLiteral>(consume().string_value()));
@@ -576,6 +579,9 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal()
return create_ast_node<TemplateLiteral>(expressions);
}
consume(TokenType::TemplateLiteralExprEnd);
+
+ if (!match(TokenType::TemplateLiteralString))
+ expressions.append(create_ast_node<StringLiteral>(""));
}
}
@@ -591,6 +597,10 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal()
NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associativity associativity)
{
auto expression = parse_primary_expression();
+ while (match(TokenType::TemplateLiteralStart)) {
+ auto template_literal = parse_template_literal();
+ expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal));
+ }
while (match_secondary_expression()) {
int new_precedence = operator_precedence(m_parser_state.m_current_token.type());
if (new_precedence < min_precedence)
@@ -600,6 +610,10 @@ NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associati
Associativity new_associativity = operator_associativity(m_parser_state.m_current_token.type());
expression = parse_secondary_expression(move(expression), new_precedence, new_associativity);
+ while (match(TokenType::TemplateLiteralStart)) {
+ auto template_literal = parse_template_literal();
+ expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal));
+ }
}
return expression;
}
diff --git a/Libraries/LibJS/Tests/tagged-template-literals.js b/Libraries/LibJS/Tests/tagged-template-literals.js
new file mode 100644
index 0000000000..7318ab5e35
--- /dev/null
+++ b/Libraries/LibJS/Tests/tagged-template-literals.js
@@ -0,0 +1,88 @@
+load("test-common.js");
+
+try {
+ assertThrowsError(() => {
+ foo`bar${baz}`;
+ }, {
+ error: ReferenceError,
+ message: "'foo' not known"
+ });
+
+ assertThrowsError(() => {
+ function foo() { }
+ foo`bar${baz}`;
+ }, {
+ error: ReferenceError,
+ message: "'baz' not known"
+ });
+
+ assertThrowsError(() => {
+ undefined``````;
+ }, {
+ error: TypeError,
+ message: "undefined is not a function"
+ });
+
+ function test1(strings) {
+ assert(strings instanceof Array);
+ assert(strings.length === 1);
+ assert(strings[0] === "");
+ return 42;
+ }
+ assert(test1`` === 42);
+
+ function test2(s) {
+ return function (strings) {
+ assert(strings instanceof Array);
+ assert(strings.length === 1);
+ assert(strings[0] === "bar");
+ return s + strings[0];
+ }
+ }
+ assert(test2("foo")`bar` === "foobar");
+
+ var test3 = {
+ foo(strings, p1) {
+ assert(strings instanceof Array);
+ assert(strings.length === 2);
+ assert(strings[0] === "");
+ assert(strings[1] === "");
+ assert(p1 === "bar");
+ }
+ };
+ test3.foo`${"bar"}`;
+
+ function test4(strings, p1) {
+ assert(strings instanceof Array);
+ assert(strings.length === 2);
+ assert(strings[0] === "foo");
+ assert(strings[1] === "");
+ assert(p1 === 42);
+ }
+ var bar = 42;
+ test4`foo${bar}`;
+
+ function test5(strings, p1, p2) {
+ assert(strings instanceof Array);
+ assert(strings.length === 3);
+ assert(strings[0] === "foo");
+ assert(strings[1] === "baz");
+ assert(strings[2] === "");
+ assert(p1 === 42);
+ assert(p2 === "qux");
+ return (strings, value) => `${value}${strings[0]}`;
+ }
+ var bar = 42;
+ assert(test5`foo${bar}baz${"qux"}``test${123}` === "123test");
+
+ function review(strings, name, rating) {
+ return `${strings[0]}**${name}**${strings[1]}_${rating}_${strings[2]}`;
+ }
+ var name = "SerenityOS";
+ var rating = "great";
+ assert(review`${name} is a ${rating} project!` === "**SerenityOS** is a _great_ project!");
+
+ console.log("PASS");
+} catch (e) {
+ console.log("FAIL: " + e);
+}