blob: d24067c352df9a1f7eb00c5650c0f7253dc1ca8b [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <iostream>
#include <sstream>
#include "tools/gn/input_file.h"
#include "tools/gn/parser.h"
#include "tools/gn/tokenizer.h"
#include "util/test/test.h"
namespace {
bool GetTokens(const InputFile* input, std::vector<Token>* result) {
result->clear();
Err err;
*result = Tokenizer::Tokenize(input, &err);
return !err.has_error();
}
void DoParserPrintTest(const char* input, const char* expected) {
std::vector<Token> tokens;
InputFile input_file(SourceFile("/test"));
input_file.SetContents(input);
ASSERT_TRUE(GetTokens(&input_file, &tokens));
Err err;
std::unique_ptr<ParseNode> result = Parser::Parse(tokens, &err);
if (!result)
err.PrintToStdout();
ASSERT_TRUE(result);
std::ostringstream collector;
RenderToText(result->GetJSONNode(), 0, collector);
EXPECT_EQ(expected, collector.str());
}
void DoExpressionPrintTest(const char* input, const char* expected) {
std::vector<Token> tokens;
InputFile input_file(SourceFile("/test"));
input_file.SetContents(input);
ASSERT_TRUE(GetTokens(&input_file, &tokens));
Err err;
std::unique_ptr<ParseNode> result = Parser::ParseExpression(tokens, &err);
ASSERT_TRUE(result);
std::ostringstream collector;
RenderToText(result->GetJSONNode(), 0, collector);
EXPECT_EQ(expected, collector.str());
}
// Expects the tokenizer or parser to identify an error at the given line and
// character.
void DoParserErrorTest(const char* input, int err_line, int err_char) {
InputFile input_file(SourceFile("/test"));
input_file.SetContents(input);
Err err;
std::vector<Token> tokens = Tokenizer::Tokenize(&input_file, &err);
if (!err.has_error()) {
std::unique_ptr<ParseNode> result = Parser::Parse(tokens, &err);
ASSERT_FALSE(result);
ASSERT_TRUE(err.has_error());
}
EXPECT_EQ(err_line, err.location().line_number());
EXPECT_EQ(err_char, err.location().column_number());
}
// Expects the tokenizer or parser to identify an error at the given line and
// character.
void DoExpressionErrorTest(const char* input, int err_line, int err_char) {
InputFile input_file(SourceFile("/test"));
input_file.SetContents(input);
Err err;
std::vector<Token> tokens = Tokenizer::Tokenize(&input_file, &err);
if (!err.has_error()) {
std::unique_ptr<ParseNode> result = Parser::ParseExpression(tokens, &err);
ASSERT_FALSE(result);
ASSERT_TRUE(err.has_error());
}
EXPECT_EQ(err_line, err.location().line_number());
EXPECT_EQ(err_char, err.location().column_number());
}
} // namespace
TEST(Parser, Literal) {
DoExpressionPrintTest("5", "LITERAL(5)\n");
DoExpressionPrintTest("\"stuff\"", "LITERAL(\"stuff\")\n");
}
TEST(Parser, BinaryOp) {
// TODO(scottmg): The tokenizer is dumb, and treats "5-1" as two integers,
// not a binary operator between two positive integers.
DoExpressionPrintTest("5 - 1",
"BINARY(-)\n"
" LITERAL(5)\n"
" LITERAL(1)\n");
DoExpressionPrintTest("5+1",
"BINARY(+)\n"
" LITERAL(5)\n"
" LITERAL(1)\n");
DoExpressionPrintTest("5 - 1 - 2",
"BINARY(-)\n"
" BINARY(-)\n"
" LITERAL(5)\n"
" LITERAL(1)\n"
" LITERAL(2)\n");
}
TEST(Parser, FunctionCall) {
DoExpressionPrintTest("foo()",
"FUNCTION(foo)\n"
" LIST\n");
DoExpressionPrintTest("blah(1, 2)",
"FUNCTION(blah)\n"
" LIST\n"
" LITERAL(1)\n"
" LITERAL(2)\n");
DoExpressionErrorTest("foo(1, 2,)", 1, 10);
DoExpressionErrorTest("foo(1 2)", 1, 7);
}
TEST(Parser, ParenExpression) {
const char* input = "(foo(1)) + (a + (b - c) + d)";
const char* expected =
"BINARY(+)\n"
" FUNCTION(foo)\n"
" LIST\n"
" LITERAL(1)\n"
" BINARY(+)\n"
" BINARY(+)\n"
" IDENTIFIER(a)\n"
" BINARY(-)\n"
" IDENTIFIER(b)\n"
" IDENTIFIER(c)\n"
" IDENTIFIER(d)\n";
DoExpressionPrintTest(input, expected);
DoExpressionErrorTest("(a +", 1, 4);
}
TEST(Parser, OrderOfOperationsLeftAssociative) {
const char* input = "5 - 1 - 2\n";
const char* expected =
"BINARY(-)\n"
" BINARY(-)\n"
" LITERAL(5)\n"
" LITERAL(1)\n"
" LITERAL(2)\n";
DoExpressionPrintTest(input, expected);
}
TEST(Parser, OrderOfOperationsEqualityBoolean) {
const char* input =
"if (a == \"b\" && is_stuff) {\n"
" print(\"hai\")\n"
"}\n";
const char* expected =
"BLOCK\n"
" CONDITION\n"
" BINARY(&&)\n"
" BINARY(==)\n"
" IDENTIFIER(a)\n"
" LITERAL(\"b\")\n"
" IDENTIFIER(is_stuff)\n"
" BLOCK\n"
" FUNCTION(print)\n"
" LIST\n"
" LITERAL(\"hai\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, UnaryOp) {
DoExpressionPrintTest("!foo",
"UNARY(!)\n"
" IDENTIFIER(foo)\n");
// No contents for binary operator.
DoExpressionErrorTest("a = !", 1, 5);
}
TEST(Parser, List) {
DoExpressionPrintTest("[]", "LIST\n");
DoExpressionPrintTest("[1,asd,]",
"LIST\n"
" LITERAL(1)\n"
" IDENTIFIER(asd)\n");
DoExpressionPrintTest("[1, 2+3 - foo]",
"LIST\n"
" LITERAL(1)\n"
" BINARY(-)\n"
" BINARY(+)\n"
" LITERAL(2)\n"
" LITERAL(3)\n"
" IDENTIFIER(foo)\n");
DoExpressionPrintTest("[1,\n2,\n 3,\n 4]",
"LIST\n"
" LITERAL(1)\n"
" LITERAL(2)\n"
" LITERAL(3)\n"
" LITERAL(4)\n");
DoExpressionErrorTest("[a, 2+,]", 1, 7);
DoExpressionErrorTest("[,]", 1, 2);
DoExpressionErrorTest("[a,,]", 1, 4);
}
TEST(Parser, Assignment) {
DoParserPrintTest("a=2",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(2)\n");
DoExpressionErrorTest("a = ", 1, 3);
}
TEST(Parser, Accessor) {
// Accessor indexing.
DoParserPrintTest("a=b[c+2]",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" ACCESSOR\n"
" b\n" // AccessorNode is a bit weird in that it holds
// a Token, not a ParseNode for the base.
" BINARY(+)\n"
" IDENTIFIER(c)\n"
" LITERAL(2)\n");
DoParserErrorTest("a = b[1][0]", 1, 5);
// Member accessors.
DoParserPrintTest("a=b.c+2",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" BINARY(+)\n"
" ACCESSOR\n"
" b\n"
" IDENTIFIER(c)\n"
" LITERAL(2)\n");
DoParserPrintTest("a.b = 5",
"BLOCK\n"
" BINARY(=)\n"
" ACCESSOR\n"
" a\n"
" IDENTIFIER(b)\n"
" LITERAL(5)\n");
DoParserErrorTest("a = b.c.d", 1, 6); // Can't nest accessors (currently).
// Error at the bad dot in the RHS, not the + operator (crbug.com/472038).
DoParserErrorTest("foo(a + b.c.d)", 1, 10);
}
TEST(Parser, Condition) {
DoParserPrintTest("if(1) { a = 2 }",
"BLOCK\n"
" CONDITION\n"
" LITERAL(1)\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(2)\n");
DoParserPrintTest("if(1) { a = 2 } else if (0) { a = 3 } else { a = 4 }",
"BLOCK\n"
" CONDITION\n"
" LITERAL(1)\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(2)\n"
" CONDITION\n"
" LITERAL(0)\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(3)\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(4)\n");
}
TEST(Parser, OnlyCallAndAssignInBody) {
DoParserErrorTest("[]", 1, 2);
DoParserErrorTest("3 + 4", 1, 5);
DoParserErrorTest("6 - 7", 1, 5);
DoParserErrorTest("if (1) { 5 } else { print(4) }", 1, 12);
}
TEST(Parser, NoAssignmentInCondition) {
DoParserErrorTest("if (a=2) {}", 1, 5);
}
TEST(Parser, CompleteFunction) {
const char* input =
"cc_test(\"foo\") {\n"
" sources = [\n"
" \"foo.cc\",\n"
" \"foo.h\"\n"
" ]\n"
" dependencies = [\n"
" \"base\"\n"
" ]\n"
"}\n";
const char* expected =
"BLOCK\n"
" FUNCTION(cc_test)\n"
" LIST\n"
" LITERAL(\"foo\")\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"foo.cc\")\n"
" LITERAL(\"foo.h\")\n"
" BINARY(=)\n"
" IDENTIFIER(dependencies)\n"
" LIST\n"
" LITERAL(\"base\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, FunctionWithConditional) {
const char* input =
"cc_test(\"foo\") {\n"
" sources = [\"foo.cc\"]\n"
" if (OS == \"mac\") {\n"
" sources += \"bar.cc\"\n"
" } else if (OS == \"win\") {\n"
" sources -= [\"asd.cc\", \"foo.cc\"]\n"
" } else {\n"
" dependencies += [\"bar.cc\"]\n"
" }\n"
"}\n";
const char* expected =
"BLOCK\n"
" FUNCTION(cc_test)\n"
" LIST\n"
" LITERAL(\"foo\")\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"foo.cc\")\n"
" CONDITION\n"
" BINARY(==)\n"
" IDENTIFIER(OS)\n"
" LITERAL(\"mac\")\n"
" BLOCK\n"
" BINARY(+=)\n"
" IDENTIFIER(sources)\n"
" LITERAL(\"bar.cc\")\n"
" CONDITION\n"
" BINARY(==)\n"
" IDENTIFIER(OS)\n"
" LITERAL(\"win\")\n"
" BLOCK\n"
" BINARY(-=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"asd.cc\")\n"
" LITERAL(\"foo.cc\")\n"
" BLOCK\n"
" BINARY(+=)\n"
" IDENTIFIER(dependencies)\n"
" LIST\n"
" LITERAL(\"bar.cc\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, UnterminatedBlock) {
DoParserErrorTest("stuff() {", 1, 9);
}
TEST(Parser, BadlyTerminatedNumber) {
DoParserErrorTest("1234z", 1, 5);
}
TEST(Parser, NewlinesInUnusualPlaces) {
DoParserPrintTest(
"if\n"
"(\n"
"a\n"
")\n"
"{\n"
"}\n",
"BLOCK\n"
" CONDITION\n"
" IDENTIFIER(a)\n"
" BLOCK\n");
}
TEST(Parser, NewlinesInUnusualPlaces2) {
DoParserPrintTest("a\n=\n2\n",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(2)\n");
DoParserPrintTest("x =\ny if\n(1\n) {}",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(x)\n"
" IDENTIFIER(y)\n"
" CONDITION\n"
" LITERAL(1)\n"
" BLOCK\n");
DoParserPrintTest("x = 3\n+2",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(x)\n"
" BINARY(+)\n"
" LITERAL(3)\n"
" LITERAL(2)\n");
}
TEST(Parser, NewlineBeforeSubscript) {
const char* input = "a = b[1]";
const char* input_with_newline = "a = b\n[1]";
const char* expected =
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" ACCESSOR\n"
" b\n"
" LITERAL(1)\n";
DoParserPrintTest(input, expected);
DoParserPrintTest(input_with_newline, expected);
}
TEST(Parser, SequenceOfExpressions) {
DoParserPrintTest("a = 1 b = 2",
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(1)\n"
" BINARY(=)\n"
" IDENTIFIER(b)\n"
" LITERAL(2)\n");
}
TEST(Parser, BlockAfterFunction) {
const char* input = "func(\"stuff\") {\n}";
// TODO(scottmg): Do we really want these to mean different things?
const char* input_with_newline = "func(\"stuff\")\n{\n}";
const char* expected =
"BLOCK\n"
" FUNCTION(func)\n"
" LIST\n"
" LITERAL(\"stuff\")\n"
" BLOCK\n";
DoParserPrintTest(input, expected);
DoParserPrintTest(input_with_newline, expected);
}
TEST(Parser, LongExpression) {
const char* input = "a = b + c && d || e";
const char* expected =
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" BINARY(||)\n"
" BINARY(&&)\n"
" BINARY(+)\n"
" IDENTIFIER(b)\n"
" IDENTIFIER(c)\n"
" IDENTIFIER(d)\n"
" IDENTIFIER(e)\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsStandalone) {
const char* input =
"# Toplevel comment.\n"
"\n"
"executable(\"wee\") {}\n";
const char* expected =
"BLOCK\n"
" BLOCK_COMMENT(# Toplevel comment.)\n"
" FUNCTION(executable)\n"
" LIST\n"
" LITERAL(\"wee\")\n"
" BLOCK\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsStandaloneEof) {
const char* input =
"executable(\"wee\") {}\n"
"# EOF comment.\n";
const char* expected =
"BLOCK\n"
" +AFTER_COMMENT(\"# EOF comment.\")\n"
" FUNCTION(executable)\n"
" LIST\n"
" LITERAL(\"wee\")\n"
" BLOCK\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsLineAttached) {
const char* input =
"executable(\"wee\") {\n"
" # Some sources.\n"
" sources = [\n"
" \"stuff.cc\",\n"
" \"things.cc\",\n"
" # This file is special or something.\n"
" \"another.cc\",\n"
" ]\n"
"}\n";
const char* expected =
"BLOCK\n"
" FUNCTION(executable)\n"
" LIST\n"
" LITERAL(\"wee\")\n"
" BLOCK\n"
" BINARY(=)\n"
" +BEFORE_COMMENT(\"# Some sources.\")\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"stuff.cc\")\n"
" LITERAL(\"things.cc\")\n"
" LITERAL(\"another.cc\")\n"
" +BEFORE_COMMENT(\"# This file is special or something.\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsSuffix) {
const char* input =
"executable(\"wee\") { # This is some stuff.\n"
"sources = [ \"a.cc\" # And another comment here.\n"
"] }";
const char* expected =
"BLOCK\n"
" FUNCTION(executable)\n"
" LIST\n"
" LITERAL(\"wee\")\n"
" END())\n"
" +SUFFIX_COMMENT(\"# This is some stuff.\")\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"a.cc\")\n"
" +SUFFIX_COMMENT(\"# And another comment here.\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsSuffixDifferentLine) {
const char* input =
"executable(\"wee\") {\n"
" sources = [ \"a\",\n"
" \"b\" ] # Comment\n"
"}\n";
const char* expected =
"BLOCK\n"
" FUNCTION(executable)\n"
" LIST\n"
" LITERAL(\"wee\")\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"a\")\n"
" LITERAL(\"b\")\n"
" END(])\n"
" +SUFFIX_COMMENT(\"# Comment\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsSuffixMultiple) {
const char* input =
"executable(\"wee\") {\n"
" sources = [\n"
" \"a\", # This is a comment,\n"
" # and some more,\n" // Note that this is aligned with above.
" # then the end.\n"
" ]\n"
"}\n";
const char* expected =
"BLOCK\n"
" FUNCTION(executable)\n"
" LIST\n"
" LITERAL(\"wee\")\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"a\")\n"
" +SUFFIX_COMMENT(\"# This is a comment,\")\n"
" +SUFFIX_COMMENT(\"# and some more,\")\n"
" +SUFFIX_COMMENT(\"# then the end.\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsConnectedInList) {
const char* input =
"defines = [\n"
"\n"
" # Connected comment.\n"
" \"WEE\",\n"
" \"BLORPY\",\n"
"]\n";
const char* expected =
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(defines)\n"
" LIST\n"
" LITERAL(\"WEE\")\n"
" +BEFORE_COMMENT(\"# Connected comment.\")\n"
" LITERAL(\"BLORPY\")\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, CommentsAtEndOfBlock) {
const char* input =
"if (is_win) {\n"
" sources = [\"a.cc\"]\n"
" # Some comment at end.\n"
"}\n";
const char* expected =
"BLOCK\n"
" CONDITION\n"
" IDENTIFIER(is_win)\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(sources)\n"
" LIST\n"
" LITERAL(\"a.cc\")\n"
" END(})\n"
" +BEFORE_COMMENT(\"# Some comment at end.\")\n";
DoParserPrintTest(input, expected);
}
// TODO(scottmg): I could be convinced this is incorrect. It's not clear to me
// which thing this comment is intended to be attached to.
TEST(Parser, CommentsEndOfBlockSingleLine) {
const char* input =
"defines = [ # EOL defines.\n"
"]\n";
const char* expected =
"BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(defines)\n"
" +SUFFIX_COMMENT(\"# EOL defines.\")\n"
" LIST\n";
DoParserPrintTest(input, expected);
}
TEST(Parser, HangingIf) {
DoParserErrorTest("if", 1, 1);
}
TEST(Parser, NegatingList) {
DoParserErrorTest("executable(\"wee\") { sources =- [ \"foo.cc\" ] }", 1, 30);
}
TEST(Parser, ConditionNoBracesIf) {
DoParserErrorTest(
"if (true)\n"
" foreach(foo, []) {}\n"
"else {\n"
" foreach(bar, []) {}\n"
"}\n",
2, 3);
}
TEST(Parser, ConditionNoBracesElse) {
DoParserErrorTest(
"if (true) {\n"
" foreach(foo, []) {}\n"
"} else\n"
" foreach(bar, []) {}\n",
4, 3);
}
TEST(Parser, ConditionNoBracesElseIf) {
DoParserErrorTest(
"if (true) {\n"
" foreach(foo, []) {}\n"
"} else if (true)\n"
" foreach(bar, []) {}\n",
4, 3);
}
// Disallow standalone {} for introducing new scopes. These are ambiguous with
// target declarations (e.g. is:
// foo("bar") {}
// a function with an associated block, or a standalone function with a
// freestanding block.
TEST(Parser, StandaloneBlock) {
// The error is reported at the end of the block when nothing is done
// with it. If we had said "a = { ..." then it would have been OK.
DoParserErrorTest(
"if (true) {\n"
"}\n"
"{\n"
" assert(false)\n"
"}\n",
5, 1);
}
TEST(Parser, BlockValues) {
const char* input =
"print({a = 1 b = 2}, 3)\n"
"a = { b = \"asd\" }";
const char* expected =
"BLOCK\n"
" FUNCTION(print)\n"
" LIST\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" LITERAL(1)\n"
" BINARY(=)\n"
" IDENTIFIER(b)\n"
" LITERAL(2)\n"
" LITERAL(3)\n"
" BINARY(=)\n"
" IDENTIFIER(a)\n"
" BLOCK\n"
" BINARY(=)\n"
" IDENTIFIER(b)\n"
" LITERAL(\"asd\")\n";
DoParserPrintTest(input, expected);
}