// 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 "tools/gn/tokenizer.h"
#include "base/logging.h"
#include "tools/gn/input_file.h"
namespace {
bool IsNumberChar(char c) {
return c >= '0' && c <= '9';
}
bool CouldBeTwoCharOperatorBegin(char c) {
return c == '<' || c == '>' || c == '!' || c == '=' || c == '-' ||
c == '+' || c == '|' || c == '&';
}
bool CouldBeTwoCharOperatorEnd(char c) {
return c == '=' || c == '|' || c == '&';
}
bool CouldBeOneCharOperator(char c) {
return c == '=' || c == '<' || c == '>' || c == '+' || c == '!' ||
c == ':' || c == '|' || c == '&' || c == '-';
}
bool CouldBeOperator(char c) {
return CouldBeOneCharOperator(c) || CouldBeTwoCharOperatorBegin(c);
}
bool IsScoperChar(char c) {
return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}';
}
Token::Type GetSpecificOperatorType(base::StringPiece value) {
if (value == "=")
return Token::EQUAL;
if (value == "+")
return Token::PLUS;
if (value == "-")
return Token::MINUS;
if (value == "+=")
return Token::PLUS_EQUALS;
if (value == "-=")
return Token::MINUS_EQUALS;
if (value == "==")
return Token::EQUAL_EQUAL;
if (value == "!=")
return Token::NOT_EQUAL;
if (value == "<=")
return Token::LESS_EQUAL;
if (value == ">=")
return Token::GREATER_EQUAL;
if (value == "<")
return Token::LESS_THAN;
if (value == ">")
return Token::GREATER_THAN;
if (value == "&&")
return Token::BOOLEAN_AND;
if (value == "||")
return Token::BOOLEAN_OR;
if (value == "!")
return Token::BANG;
return Token::INVALID;
}
} // namespace
Tokenizer::Tokenizer(const InputFile* input_file, Err* err)
: input_file_(input_file),
input_(input_file->contents()),
err_(err),
cur_(0),
line_number_(1),
char_in_line_(1) {
}
Tokenizer::~Tokenizer() {
}
// static
std::vector<Token> Tokenizer::Tokenize(const InputFile* input_file, Err* err) {
Tokenizer t(input_file, err);
return t.Run();
}
std::vector<Token> Tokenizer::Run() {
DCHECK(tokens_.empty());
while (!done()) {
AdvanceToNextToken();
if (done())
break;
Location location = GetCurrentLocation();
Token::Type type = ClassifyCurrent();
if (type == Token::INVALID) {
*err_ = GetErrorForInvalidToken(location);
break;
}
size_t token_begin = cur_;
AdvanceToEndOfToken(location, type);
if (has_error())
break;
size_t token_end = cur_;
base::StringPiece token_value(&input_.data()[token_begin],
token_end - token_begin);
if (type == Token::UNCLASSIFIED_OPERATOR)
type = GetSpecificOperatorType(token_value);
if (type == Token::IDENTIFIER) {
if (token_value == "if")
type = Token::IF;
else if (token_value == "else")
type = Token::ELSE;
else if (token_value == "true")
type = Token::TRUE_TOKEN;
else if (token_value == "false")
type = Token::FALSE_TOKEN;
}
// TODO(brettw) This just strips comments from the token stream. This
// is probably wrong, they should be removed at a later stage so we can
// do things like rewrite the file. But this makes the parser simpler and
// is OK for now.
if (type != Token::COMMENT)
tokens_.push_back(Token(location, type, token_value));
}
if (err_->has_error())
tokens_.clear();
return tokens_;
}
// static
size_t Tokenizer::ByteOffsetOfNthLine(const base::StringPiece& buf, int n) {
int cur_line = 1;
size_t cur_byte = 0;
DCHECK(n > 0);
if (n == 1)
return 0;
while (cur_byte < buf.size()) {
if (IsNewline(buf, cur_byte)) {
cur_line++;
if (cur_line == n)
return cur_byte + 1;
}
cur_byte++;
}
return -1;
}
// static
bool Tokenizer::IsNewline(const base::StringPiece& buffer, size_t offset) {
DCHECK(offset < buffer.size());
// We may need more logic here to handle different line ending styles.
return buffer[offset] == '\n';
}
void Tokenizer::AdvanceToNextToken() {
while (!at_end() && IsCurrentWhitespace())
Advance();
}
Token::Type Tokenizer::ClassifyCurrent() const {
DCHECK(!at_end());
char next_char = cur_char();
if (next_char >= '0' && next_char <= '9')
return Token::INTEGER;
if (next_char == '"')
return Token::STRING;
// Note: '-' handled specially below.
if (next_char != '-' && CouldBeOperator(next_char))
return Token::UNCLASSIFIED_OPERATOR;
if (IsIdentifierFirstChar(next_char))
return Token::IDENTIFIER;
if (next_char == '[')
return Token::LEFT_BRACKET;
if (next_char == ']')
return Token::RIGHT_BRACKET;
if (next_char == '(')
return Token::LEFT_PAREN;
if (next_char == ')')
return Token::RIGHT_PAREN;
if (next_char == '{')
return Token::LEFT_BRACE;
if (next_char == '}')
return Token::RIGHT_BRACE;
if (next_char == ',')
return Token::COMMA;
if (next_char == '#')
return Token::COMMENT;
// For the case of '-' differentiate between a negative number and anything
// else.
if (next_char == '-') {
if (!CanIncrement())
return Token::UNCLASSIFIED_OPERATOR; // Just the minus before end of
// file.
char following_char = input_[cur_ + 1];
if (following_char >= '0' && following_char <= '9')
return Token::INTEGER;
return Token::UNCLASSIFIED_OPERATOR;
}
return Token::INVALID;
}
void Tokenizer::AdvanceToEndOfToken(const Location& location,
Token::Type type) {
switch (type) {
case Token::INTEGER:
do {
Advance();
} while (!at_end() && IsNumberChar(cur_char()));
if (!at_end()) {
// Require the char after a number to be some kind of space, scope,
// or operator.
char c = cur_char();
if (!IsCurrentWhitespace() && !CouldBeOperator(c) &&
!IsScoperChar(c) && c != ',') {
*err_ = Err(GetCurrentLocation(),
"This is not a valid number.",
"Learn to count.");
// Highlight the number.
err_->AppendRange(LocationRange(location, GetCurrentLocation()));
}
}
break;
case Token::STRING: {
char initial = cur_char();
Advance(); // Advance past initial "
for (;;) {
if (at_end()) {
*err_ = Err(LocationRange(location,
Location(input_file_, line_number_, char_in_line_)),
"Unterminated string literal.",
"Don't leave me hanging like this!");
break;
}
if (IsCurrentStringTerminator(initial)) {
Advance(); // Skip past last "
break;
} else if (cur_char() == '\n') {
*err_ = Err(LocationRange(location,
GetCurrentLocation()),
"Newline in string constant.");
}
Advance();
}
break;
}
case Token::UNCLASSIFIED_OPERATOR:
// Some operators are two characters, some are one.
if (CouldBeTwoCharOperatorBegin(cur_char())) {
if (CanIncrement() && CouldBeTwoCharOperatorEnd(input_[cur_ + 1]))
Advance();
}
Advance();
break;
case Token::IDENTIFIER:
while (!at_end() && IsIdentifierContinuingChar(cur_char()))
Advance();
break;
case Token::LEFT_BRACKET:
case Token::RIGHT_BRACKET:
case Token::LEFT_BRACE:
case Token::RIGHT_BRACE:
case Token::LEFT_PAREN:
case Token::RIGHT_PAREN:
case Token::COMMA:
Advance(); // All are one char.
break;
case Token::COMMENT:
// Eat to EOL.
while (!at_end() && !IsCurrentNewline())
Advance();
break;
case Token::INVALID:
default:
*err_ = Err(location, "Everything is all messed up",
"Please insert system disk in drive A: and press any key.");
NOTREACHED();
return;
}
}
bool Tokenizer::IsCurrentWhitespace() const {
DCHECK(!at_end());
char c = input_[cur_];
// Note that tab (0x09) is illegal.
return c == 0x0A || c == 0x0B || c == 0x0C || c == 0x0D || c == 0x20;
}
bool Tokenizer::IsCurrentStringTerminator(char quote_char) const {
DCHECK(!at_end());
if (cur_char() != quote_char)
return false;
// Check for escaping. \" is not a string terminator, but \\" is. Count
// the number of preceeding backslashes.
int num_backslashes = 0;
for (int i = static_cast<int>(cur_) - 1; i >= 0 && input_[i] == '\\'; i--)
num_backslashes++;
// Even backslashes mean that they were escaping each other and don't count
// as escaping this quote.
return (num_backslashes % 2) == 0;
}
bool Tokenizer::IsCurrentNewline() const {
return IsNewline(input_, cur_);
}
void Tokenizer::Advance() {
DCHECK(cur_ < input_.size());
if (IsCurrentNewline()) {
line_number_++;
char_in_line_ = 1;
} else {
char_in_line_++;
}
cur_++;
}
Location Tokenizer::GetCurrentLocation() const {
return Location(input_file_, line_number_, char_in_line_);
}
Err Tokenizer::GetErrorForInvalidToken(const Location& location) const {
std::string help;
if (cur_char() == ';') {
// Semicolon.
help = "Semicolons are not needed, delete this one.";
} else if (cur_char() == '\t') {
// Tab.
help = "You got a tab character in here. Tabs are evil. "
"Convert to spaces.";
} else if (cur_char() == '/' && cur_ + 1 < input_.size() &&
(input_[cur_ + 1] == '/' || input_[cur_ + 1] == '*')) {
// Different types of comments.
help = "Comments should start with # instead";
} else {
help = "I have no idea what this is.";
}
return Err(location, "Invalid token.", help);
}