com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase.java Source code

Java tutorial

Introduction

Here is the source code for com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase.java

Source

/*
 * Copyright 2016 The Bazel Authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.idea.blaze.base.lang.buildfile.lexer;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import javax.annotation.Nullable;

/**
 * A tokenizer for the BUILD language.
 *
 * <p>Copied from blaze/bazel's lexer. The differences are: 1. Blaze's lexer isn't 'faithful', in
 * that it reorders characters, skips characters, and adds ghost characters. We can't do that,
 * because we need to match the editor's view of the document. 2. Blaze's lexer only lexes entire
 * files (it can't incrementally lex part of a file, starting from a given indent stack depth).
 */
public class BuildLexerBase {

    /**
     * When tokenizing for the purposes of parsing, we handle indentation.<br>
     * For syntax highlighting, we need to tokenize every character, and don't care about indentation.
     */
    public enum LexerMode {
        Parsing, SyntaxHighlighting
    }

    // Characters that can come immediately prior to an '=' character to generate
    // a different token
    private static final Map<Character, TokenKind> EQUAL_TOKENS = ImmutableMap.<Character, TokenKind>builder()
            .put('=', TokenKind.EQUALS_EQUALS).put('!', TokenKind.NOT_EQUALS).put('>', TokenKind.GREATER_EQUALS)
            .put('<', TokenKind.LESS_EQUALS).put('+', TokenKind.PLUS_EQUALS).put('-', TokenKind.MINUS_EQUALS)
            .put('*', TokenKind.STAR_EQUALS).put('/', TokenKind.SLASH_EQUALS).put('%', TokenKind.PERCENT_EQUALS)
            .build();

    private final LexerMode mode;

    // Input buffer and position
    private final char[] buffer;
    private int pos;

    private final List<Token> tokens;

    // The number of unclosed open-parens ("(", '{', '[') at the current point in
    // the stream. Whitespace is handled differently when this is nonzero.
    private int openParenStackDepth = 0;

    // The stack of enclosing indentation levels; always contains '0' at the
    // bottom.
    private final Stack<Integer> indentStack = new Stack<>();

    private boolean containsErrors;

    /**
     * Constructs a lexer which tokenizes the contents of the specified InputBuffer. Any errors during
     * lexing are reported on "handler".
     */
    public BuildLexerBase(CharSequence input, int initialStackDepth, LexerMode mode) {
        this.buffer = input.toString().toCharArray();
        // Empirical measurements show roughly 1 token per 8 characters in buffer.
        this.tokens = Lists.newArrayListWithExpectedSize(buffer.length / 8);
        this.pos = 0;
        this.openParenStackDepth = initialStackDepth;
        this.mode = mode;

        indentStack.push(0);
        tokenize();
    }

    /** The number of unclosed open-parens ("(", '{', '[') at the end of this string. */
    public int getOpenParenStackDepth() {
        return openParenStackDepth;
    }

    /**
     * Returns true if there were errors during scanning of this input file or string. The
     * BuildLexerBase may attempt to recover from errors, but clients should not rely on the results
     * of scanning if this flag is set.
     */
    public boolean containsErrors() {
        return containsErrors;
    }

    /** Returns the (mutable) list of tokens generated by the BuildLexerBase. */
    public List<Token> getTokens() {
        return tokens;
    }

    private void popParen() {
        if (openParenStackDepth == 0) {
            error("indentation error");
        } else {
            openParenStackDepth--;
        }
    }

    private void error(String message) {
        error(message, pos - 1, pos - 1);
    }

    protected void error(String message, int start, int end) {
        this.containsErrors = true;
    }

    /** invariant: symbol positions are half-open intervals. */
    private void addToken(TokenKind kind, int left, int right) {
        addToken(kind, left, right, null);
    }

    private void addToken(TokenKind kind, int left, int right, @Nullable Object value) {
        tokens.add(new Token(kind, left, right, value));
    }

    /**
     * Parses an end-of-line sequence, handling statement indentation correctly.
     *
     * <p>UNIX newlines are assumed (LF). Carriage returns are always ignored.
     *
     * <p>ON ENTRY: 'pos' is the index of the char after '\n'. ON EXIT: 'pos' is the index of the next
     * non-space char after '\n'.
     */
    protected void newline() {
        if (mode == LexerMode.SyntaxHighlighting) {
            addToken(TokenKind.NEWLINE, pos - 1, pos);
            return;
        }
        if (openParenStackDepth > 0) {
            newlineInsideExpression(); // in an expression: ignore space
        } else {
            newlineOutsideExpression(); // generate NEWLINE/INDENT/DEDENT tokens
        }
    }

    private void newlineInsideExpression() {
        int oldPos = pos - 1;
        while (pos < buffer.length) {
            switch (buffer[pos]) {
            case ' ':
            case '\t':
            case '\r':
                pos++;
                break;
            default:
                // ignored by the parser
                addToken(TokenKind.WHITESPACE, oldPos, pos);
                return;
            }
        }
        addToken(TokenKind.WHITESPACE, oldPos, pos);
    }

    /**
     * Handle INDENT and DEDENT within statements.
     *
     * <p>Note these tokens have zero length -- this is because we can have an arbitrary number of
     * dedents squeezed into some number of characters, and the size of all the lexical elements must
     * match the number of characters in the file.
     */
    private void newlineOutsideExpression() {
        int oldPos = pos - 1;
        if (pos > 1) { // skip over newline at start of file
            addToken(TokenKind.NEWLINE, oldPos, pos);
            oldPos = pos;
        }

        // we're in a stmt: suck up space at beginning of next line
        int indentLen = 0;
        while (pos < buffer.length) {
            char c = buffer[pos];
            if (c == ' ') {
                indentLen++;
                pos++;
            } else if (c == '\t') {
                indentLen += 8 - indentLen % 8;
                pos++;
            } else if (c == '\n') { // entirely blank line: ignore
                indentLen = 0;
                pos++;
            } else if (c == '#') { // line containing only indented comment
                if (oldPos != pos) {
                    addToken(TokenKind.WHITESPACE, oldPos, pos);
                    oldPos = pos;
                }
                while (pos < buffer.length && c != '\n') {
                    c = buffer[pos++];
                }
                addToken(TokenKind.COMMENT, oldPos, pos - 1, bufferSlice(oldPos, pos - 1));
                oldPos = pos - 1;
                indentLen = 0;
            } else { // printing character
                break;
            }
        }

        if (oldPos != pos) {
            addToken(TokenKind.WHITESPACE, oldPos, pos);
        }
        if (pos == buffer.length) {
            indentLen = 0;
        } // trailing space on last line

        int peekedIndent = indentStack.peek();
        if (peekedIndent < indentLen) { // push a level
            indentStack.push(indentLen);
            addToken(TokenKind.INDENT, pos, pos);

        } else if (peekedIndent > indentLen) { // pop one or more levels
            while (peekedIndent > indentLen) {
                indentStack.pop();
                addToken(TokenKind.DEDENT, pos, pos);
                peekedIndent = indentStack.peek();
            }

            if (peekedIndent < indentLen) {
                error("indentation error");
            }
        }
    }

    /** Collapse adjacent whitespace characters into a single token */
    private void addWhitespace() {
        int oldPos = pos - 1;
        while (pos < buffer.length) {
            switch (buffer[pos]) {
            case ' ':
            case '\t':
            case '\r':
                pos++;
                break;
            default:
                addToken(TokenKind.WHITESPACE, oldPos, pos, bufferSlice(oldPos, pos));
                return;
            }
        }
        addToken(TokenKind.WHITESPACE, oldPos, pos, bufferSlice(oldPos, pos));
    }

    /**
     * Returns true if current position is in the middle of a triple quote delimiter (3 x quot), and
     * advances 'pos' by two if so.
     */
    private boolean skipTripleQuote(char quot) {
        if (pos + 1 < buffer.length && buffer[pos] == quot && buffer[pos + 1] == quot) {
            pos += 2;
            return true;
        } else {
            return false;
        }
    }

    /**
     * Scans a string literal delimited by 'quot', containing escape sequences.
     *
     * <p>ON ENTRY: 'pos' is 1 + the index of the first delimiter ON EXIT: 'pos' is 1 + the index of
     * the last delimiter.
     */
    private void escapedStringLiteral(char quot, boolean isRaw) {
        int oldPos = isRaw ? pos - 2 : pos - 1;
        boolean inTripleQuote = skipTripleQuote(quot);

        // more expensive second choice that expands escaped into a buffer
        StringBuilder literal = new StringBuilder();
        while (pos < buffer.length) {
            char c = buffer[pos];
            pos++;
            switch (c) {
            case '\n':
                if (inTripleQuote) {
                    literal.append(c);
                    break;
                } else {
                    error("unterminated string literal at eol", oldPos, pos);
                    addToken(TokenKind.STRING, oldPos, pos - 1, literal.toString());
                    newline();
                    return;
                }
            case '\\':
                if (pos == buffer.length) {
                    error("unterminated string literal at eof", oldPos, pos);
                    addToken(TokenKind.STRING, oldPos, pos - 1, literal.toString());
                    return;
                }
                if (isRaw) {
                    // Insert \ and the following character.
                    // As in Python, it means that a raw string can never end with a single \.
                    literal.append('\\');
                    literal.append(buffer[pos]);
                    pos++;
                    break;
                }
                c = buffer[pos];
                pos++;
                switch (c) {
                case '\n':
                    // ignore end of line character
                    break;
                case 'n':
                    literal.append('\n');
                    break;
                case 'r':
                    literal.append('\r');
                    break;
                case 't':
                    literal.append('\t');
                    break;
                case '\\':
                    literal.append('\\');
                    break;
                case '\'':
                    literal.append('\'');
                    break;
                case '"':
                    literal.append('"');
                    break;
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7': { // octal escape
                    int octal = c - '0';
                    if (pos < buffer.length) {
                        c = buffer[pos];
                        if (c >= '0' && c <= '7') {
                            pos++;
                            octal = (octal << 3) | (c - '0');
                            if (pos < buffer.length) {
                                c = buffer[pos];
                                if (c >= '0' && c <= '7') {
                                    pos++;
                                    octal = (octal << 3) | (c - '0');
                                }
                            }
                        }
                    }
                    literal.append((char) (octal & 0xff));
                    break;
                }
                case 'a':
                case 'b':
                case 'f':
                case 'N':
                case 'u':
                case 'U':
                case 'v':
                case 'x':
                    // exists in Python but not implemented in Blaze => error
                    error("escape sequence not implemented: \\" + c, oldPos, pos);
                    break;
                default:
                    // unknown char escape => "\literal"
                    literal.append('\\');
                    literal.append(c);
                    break;
                }
                break;
            case '\'':
            case '"':
                if (c != quot || (inTripleQuote && !skipTripleQuote(quot))) {
                    // Non-matching quote, treat it like a regular char.
                    literal.append(c);
                } else {
                    // Matching close-delimiter, all done.
                    addToken(TokenKind.STRING, oldPos, pos, literal.toString());
                    return;
                }
                break;
            default:
                literal.append(c);
                break;
            }
        }
        error("unterminated string literal at eof", oldPos, pos);
        addToken(TokenKind.STRING, oldPos, pos, literal.toString());
    }

    /**
     * Scans a string literal delimited by 'quot'.
     *
     * <ul>
     * <li> ON ENTRY: 'pos' is 1 + the index of the first delimiter
     * <li> ON EXIT: 'pos' is 1 + the index of the last delimiter.
     * </ul>
     *
     * @param isRaw if true, do not escape the string.
     */
    private void addStringLiteral(char quot, boolean isRaw) {
        int oldPos = isRaw ? pos - 2 : pos - 1;
        int start = pos;

        // Don't even attempt to parse triple-quotes here.
        if (skipTripleQuote(quot)) {
            pos -= 2;
            escapedStringLiteral(quot, isRaw);
            return;
        }

        // first quick optimistic scan for a simple non-escaped string
        while (pos < buffer.length) {
            char c = buffer[pos++];
            switch (c) {
            case '\n':
                error("unterminated string literal at eol", oldPos, pos);
                addToken(TokenKind.STRING, oldPos, pos - 1, bufferSlice(start, pos - 1));
                newline();
                return;
            case '\\':
                if (isRaw) {
                    // skip the next character
                    pos++;
                    break;
                }
                // oops, hit an escape, need to start over & build a new string buffer
                pos = oldPos + 1;
                escapedStringLiteral(quot, false);
                return;
            case '\'':
            case '"':
                if (c == quot) {
                    // close-quote, all done.
                    addToken(TokenKind.STRING, oldPos, pos, bufferSlice(start, pos - 1));
                    return;
                }
            }
        }

        error("unterminated string literal at eof", oldPos, pos);
        addToken(TokenKind.STRING, oldPos, pos, bufferSlice(start, pos));
    }

    private static final ImmutableMap<String, TokenKind> KEYWORD_MAP = createKeywordMap();

    private static ImmutableMap<String, TokenKind> createKeywordMap() {
        ImmutableMap.Builder<String, TokenKind> builder = ImmutableMap.builder();
        for (TokenKind kind : TokenKind.KEYWORDS) {
            builder.put(kind.toString(), kind);
        }
        return builder.build();
    }

    private TokenKind getTokenKindForIdentfier(String id) {
        TokenKind kind = KEYWORD_MAP.get(id);
        return kind == null ? TokenKind.IDENTIFIER : kind;
    }

    private String scanIdentifier() {
        int oldPos = pos - 1;
        while (pos < buffer.length) {
            switch (buffer[pos]) {
            case '_':
            case 'a':
            case 'b':
            case 'c':
            case 'd':
            case 'e':
            case 'f':
            case 'g':
            case 'h':
            case 'i':
            case 'j':
            case 'k':
            case 'l':
            case 'm':
            case 'n':
            case 'o':
            case 'p':
            case 'q':
            case 'r':
            case 's':
            case 't':
            case 'u':
            case 'v':
            case 'w':
            case 'x':
            case 'y':
            case 'z':
            case 'A':
            case 'B':
            case 'C':
            case 'D':
            case 'E':
            case 'F':
            case 'G':
            case 'H':
            case 'I':
            case 'J':
            case 'K':
            case 'L':
            case 'M':
            case 'N':
            case 'O':
            case 'P':
            case 'Q':
            case 'R':
            case 'S':
            case 'T':
            case 'U':
            case 'V':
            case 'W':
            case 'X':
            case 'Y':
            case 'Z':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                pos++;
                break;
            default:
                return bufferSlice(oldPos, pos);
            }
        }
        return bufferSlice(oldPos, pos);
    }

    /**
     * Scans an identifier or keyword.
     *
     * <p>ON ENTRY: 'pos' is 1 + the index of the first char in the identifier. ON EXIT: 'pos' is 1 +
     * the index of the last char in the identifier.
     *
     * @return the identifier or keyword token.
     */
    private void addIdentifierOrKeyword() {
        int oldPos = pos - 1;
        String id = scanIdentifier();
        TokenKind kind = getTokenKindForIdentfier(id);
        addToken(kind, oldPos, pos, (kind == TokenKind.IDENTIFIER) ? id : null);
    }

    private String scanInteger() {
        int oldPos = pos - 1;
        while (pos < buffer.length) {
            char c = buffer[pos];
            switch (c) {
            case 'X':
            case 'x':
            case 'a':
            case 'A':
            case 'b':
            case 'B':
            case 'c':
            case 'C':
            case 'd':
            case 'D':
            case 'e':
            case 'E':
            case 'f':
            case 'F':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                pos++;
                break;
            default:
                return bufferSlice(oldPos, pos);
            }
        }
        return bufferSlice(oldPos, pos);
    }

    /**
     * Scans an addInteger literal.
     *
     * <p>ON ENTRY: 'pos' is 1 + the index of the first char in the literal. ON EXIT: 'pos' is 1 + the
     * index of the last char in the literal.
     */
    private void addInteger() {
        int oldPos = pos - 1;
        String literal = scanInteger();

        final String substring;
        final int radix;
        if (literal.startsWith("0x") || literal.startsWith("0X")) {
            radix = 16;
            substring = literal.substring(2);
        } else if (literal.startsWith("0") && literal.length() > 1) {
            radix = 8;
            substring = literal.substring(1);
        } else {
            radix = 10;
            substring = literal;
        }

        int value = 0;
        try {
            value = Integer.parseInt(substring, radix);
        } catch (NumberFormatException e) {
            error("invalid base-" + radix + " integer constant: " + literal);
        }

        addToken(TokenKind.INT, oldPos, pos, value);
    }

    /**
     * Tokenizes a two-char operator.
     *
     * @return true if it tokenized an operator
     */
    private boolean tokenizeTwoChars() {
        if (pos + 2 >= buffer.length) {
            return false;
        }
        char c1 = buffer[pos];
        char c2 = buffer[pos + 1];
        TokenKind tok = null;
        if (c2 == '=') {
            tok = EQUAL_TOKENS.get(c1);
        } else if (c2 == '*' && c1 == '*') {
            tok = TokenKind.STAR_STAR;
        }
        if (tok == null) {
            return false;
        }
        addToken(tok, pos, pos + 2);
        return true;
    }

    /** Performs tokenization of the character buffer of file contents provided to the constructor. */
    private void tokenize() {
        while (pos < buffer.length) {
            if (tokenizeTwoChars()) {
                pos += 2;
                continue;
            }
            char c = buffer[pos];
            pos++;
            switch (c) {
            case '{': {
                addToken(TokenKind.LBRACE, pos - 1, pos);
                openParenStackDepth++;
                break;
            }
            case '}': {
                addToken(TokenKind.RBRACE, pos - 1, pos);
                popParen();
                break;
            }
            case '(': {
                addToken(TokenKind.LPAREN, pos - 1, pos);
                openParenStackDepth++;
                break;
            }
            case ')': {
                addToken(TokenKind.RPAREN, pos - 1, pos);
                popParen();
                break;
            }
            case '[': {
                addToken(TokenKind.LBRACKET, pos - 1, pos);
                openParenStackDepth++;
                break;
            }
            case ']': {
                addToken(TokenKind.RBRACKET, pos - 1, pos);
                popParen();
                break;
            }
            case '>': {
                addToken(TokenKind.GREATER, pos - 1, pos);
                break;
            }
            case '<': {
                addToken(TokenKind.LESS, pos - 1, pos);
                break;
            }
            case ':': {
                addToken(TokenKind.COLON, pos - 1, pos);
                break;
            }
            case ',': {
                addToken(TokenKind.COMMA, pos - 1, pos);
                break;
            }
            case '+': {
                addToken(TokenKind.PLUS, pos - 1, pos);
                break;
            }
            case '-': {
                addToken(TokenKind.MINUS, pos - 1, pos);
                break;
            }
            case '|': {
                addToken(TokenKind.PIPE, pos - 1, pos);
                break;
            }
            case '=': {
                addToken(TokenKind.EQUALS, pos - 1, pos);
                break;
            }
            case '%': {
                addToken(TokenKind.PERCENT, pos - 1, pos);
                break;
            }
            case '/': {
                addToken(TokenKind.SLASH, pos - 1, pos);
                break;
            }
            case ';': {
                addToken(TokenKind.SEMI, pos - 1, pos);
                break;
            }
            case '.': {
                addToken(TokenKind.DOT, pos - 1, pos);
                break;
            }
            case '*': {
                addToken(TokenKind.STAR, pos - 1, pos);
                break;
            }
            case ' ':
            case '\t':
            case '\r': {
                addWhitespace();
                break;
            }
            case '\\': {
                // Backslash character is valid only at the end of a line (or in a string)
                if (pos + 1 < buffer.length && buffer[pos] == '\n') {
                    // treat end of line backslash and newline char as whitespace
                    // (they're ignored by the parser)
                    pos++;
                    addToken(TokenKind.WHITESPACE, pos - 2, pos, Character.toString(c));
                } else {
                    addToken(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c));
                }
                break;
            }
            case '\n': {
                newline();
                break;
            }
            case '#': {
                int oldPos = pos - 1;
                while (pos < buffer.length) {
                    c = buffer[pos];
                    if (c == '\n') {
                        break;
                    } else {
                        pos++;
                    }
                }
                addToken(TokenKind.COMMENT, oldPos, pos, bufferSlice(oldPos, pos));
                break;
            }
            case '\'':
            case '\"': {
                addStringLiteral(c, false);
                break;
            }
            default: {
                // detect raw strings, e.g. r"str"
                if (c == 'r' && pos < buffer.length && (buffer[pos] == '\'' || buffer[pos] == '\"')) {
                    c = buffer[pos];
                    pos++;
                    addStringLiteral(c, true);
                    break;
                }

                if (Character.isDigit(c)) {
                    addInteger();
                } else if (Character.isJavaIdentifierStart(c) && c != '$') {
                    addIdentifierOrKeyword();
                } else {
                    // Some characters in Python are not recognized in Blaze syntax (e.g. '!')
                    addToken(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c));
                    error("invalid character: '" + c + "'");
                }
                break;
            } // default
            } // switch
        } // while
    }

    /**
     * Returns parts of the source buffer based on offsets
     *
     * @param start the beginning offset for the slice
     * @param end the offset immediately following the slice
     * @return the text at offset start with length end - start
     */
    private String bufferSlice(int start, int end) {
        return new String(this.buffer, start, end - start);
    }
}