Просмотр исходного кода

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.
Linus Groh 5 лет назад
Родитель
Сommit
4d20cf57db

+ 41 - 3
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);

+ 18 - 0
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)

+ 14 - 0
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;
 }

+ 88 - 0
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);
+}