All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.api.tools.framework.snippet.SnippetParser Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2016 Google Inc.
 *
 * 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.api.tools.framework.snippet;

import com.google.api.tools.framework.snippet.Elem.Block;
import com.google.api.tools.framework.snippet.Elem.Case;
import com.google.api.tools.framework.snippet.Elem.Cond;
import com.google.api.tools.framework.snippet.Elem.Lit;
import com.google.api.tools.framework.snippet.Elem.Operator;
import com.google.api.tools.framework.snippet.Elem.Switch;
import com.google.api.tools.framework.snippet.Snippet.SnippetKind;
import com.google.api.tools.framework.snippet.SnippetSet.EvalException;
import com.google.api.tools.framework.snippet.SnippetSet.InputSupplier;
import com.google.api.tools.framework.snippet.SnippetSet.Issue;
import com.google.api.tools.framework.snippet.SnippetSet.SnippetKey;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

/**
 * Internal class for parsing snippets.
 */
class SnippetParser {

  private static final Pattern FIND_INDENT = Pattern.compile("^\\s*");
  private static final Pattern SKIP_LINE = Pattern.compile("\\s*#.*");
  private static final Pattern COMMAND_LINE = Pattern.compile("^\\s*@(\\w+)");
  private static final int COMMAND_NAME_GROUP = 1;

  private static final Pattern IDENTIFIER = Pattern.compile("\\w+");
  private static final Pattern STRING_LITERAL = Pattern.compile("\"((\\\\\"|[^\"])*)\"");
  private static final Pattern INT_LITERAL = Pattern.compile("[0-9]+");
  private static final Pattern SEPARATOR = Pattern.compile("[)(.,:=!<>]");
  private static final Pattern LITERAL = Pattern.compile(String.format(
      "%s|%s",
      STRING_LITERAL.pattern(),
      INT_LITERAL.pattern()));
  private static final Pattern EXPR_TOKEN = Pattern.compile(String.format(
      "\\s*(%s|%s|%s)",
      LITERAL.pattern(),
      IDENTIFIER.pattern(),
      SEPARATOR.pattern()));
  private static final int EXPR_TOKEN_GROUP = 1;

  private static final String SNIPPET_COMMAND = "snippet";
  private static final String OVERRIDE_COMMAND = "override";
  private static final String ABSTRACT_COMMAND = "abstract";
  private static final String PRIVATE_COMMAND = "private";
  private static final String END_COMMAND = "end";
  private static final String IF_COMMAND = "if";
  private static final String ELSE_COMMAND = "else";
  private static final String GROUP_COMMAND = "group";
  private static final String JOIN_COMMAND = "join";
  private static final String LET_COMMAND = "let";
  private static final String SWITCH_COMMAND = "switch";
  private static final String CASE_COMMAND = "case";
  private static final String DEFAULT_COMMAND = "default";
  private static final String EXTENDS_COMMAND = "extends";
  private static final String JOIN_SEPARATOR = "on";

  private static class Input {
    private final String name;
    private final Iterator lines;
    @Nullable private final Input parent;
    private int lineNo;
    private boolean noExtendsAllowed;

    private Input(String name, Iterator lines, Input parent) {
      this.name = name;
      this.lines = lines;
      this.parent = parent;
      this.lineNo = 0;
    }

    private String getPath() {
      if (parent != null) {
        return parent.getPath() + ':' + name;
      }
      return name;
    }

    private Location location() {
      return Location.create(name, lineNo);
    }
  }

  private final InputSupplier inputSupplier;
  private final List errors = Lists.newArrayList();
  private final SnippetSet snippetSet = new SnippetSet();
  private final Set inputsIncluded = Sets.newHashSet();
  private Input input;
  private String lastTerminator;
  private String lastTerminatorHeader;

  /**
   * Constructs a snippet parser for the given input.
   */
  SnippetParser(InputSupplier inputSupplier, String inputName) {
    this.inputSupplier = inputSupplier;
    this.input = openInput(inputName);
  }

  /**
   * Open input.
   */
  private Input openInput(String name) {
    Iterable lines;
    try {
      lines = inputSupplier.readInput(name);
      if (lines == null) {
        error("cannot open source '%s'", name);
        lines = ImmutableList.of();
      }
    } catch (IOException e) {
      error("cannot open source '%s': %s", name, e.getMessage());
      lines = ImmutableList.of();
    }
    return new Input(name, lines.iterator(), input);
  }

  /**
   * Runs the parser on the configured input.
   */
  void parse() {
    String line;
    while ((line = getNextLine()) != null) {

      // See if it is a command.
      Matcher matcher = COMMAND_LINE.matcher(line);
      if (matcher.find()) {

        // Extract command name and rest of command line.
        String command = matcher.group(COMMAND_NAME_GROUP);
        String rest = line.substring(matcher.end());

        switch (command) {
          case EXTENDS_COMMAND:
            parseExtends(rest);
            break;
          case SNIPPET_COMMAND:
            input.noExtendsAllowed = true;
            parseSnippet(SnippetKind.REGULAR, rest);
            break;
          case OVERRIDE_COMMAND:
            input.noExtendsAllowed = true;
            parseSnippet(SnippetKind.OVERRIDE, rest);
            break;
          case ABSTRACT_COMMAND:
            input.noExtendsAllowed = true;
            parseSnippet(SnippetKind.ABSTRACT, rest);
            break;
          case PRIVATE_COMMAND:
            input.noExtendsAllowed = true;
            parseSnippet(SnippetKind.PRIVATE, rest);
            break;
          default:
            unexpectedCommandError(command);
            break;
        }
      } else if (!CharMatcher.whitespace().matchesAllOf(line)) {

        // Report that the input line is unrecognized.
        error("unrecognized input line on top level: '%s'", line);
      }
    }
  }

  /**
   * Returns accumulated parsing errors.
   */
  List errors() {
    return errors;
  }

  /**
   * Returns the parsing result as a snippet set.
   */
  SnippetSet result() {
    return snippetSet;
  }

  private void parseExtends(String header) {

    // Parse header.
    TokenStream tokens = new TokenStream(header);
    String inputName = tokens.expect(STRING_LITERAL);
    tokens.checkAtEnd();
    if (input.noExtendsAllowed) {
      error("'@extends' commands only allowed at the beginning of the source");
    } else if (inputName != null && inputsIncluded.add(inputName)) {

      // Open the input. It will be closed automatically when the last line is read.
      input = openInput(trimLiteral(inputName));
    }
  }

  /**
   * Parses a snippet.
   */
  private void parseSnippet(SnippetKind snippetKind, String header) {

    // Parse header.
    TokenStream tokens = new TokenStream(header);

    // Expect the snippet name.
    String name = tokens.expect(IDENTIFIER);
    List paramList = Lists.newArrayList();

    // Remember the parameter names used, so we can report errors for duplicate params.
    Set usedParamNames = Sets.newHashSet();

    // Must have a parameter list.
    tokens.expect("(");

    if (!tokens.has(")")) {
      for (;;) {
        String param = tokens.expect(IDENTIFIER);
        if (!usedParamNames.add(param)) {
          error("duplicate parameter '%s'", param);
        }
        if (param != null) {
          paramList.add(param);
        }

        // See whether there are more parameters.
        if (tokens.has(",")) {
          tokens.next();
        } else {
          break;
        }
      }
    }
    tokens.expect(")");

    // Parse modifiers
    List elems = ImmutableList.of();
    Layout layout = Layout.DEFAULT;

    if (snippetKind != SnippetKind.ABSTRACT) {
      layout = parseLayout(tokens);
      tokens.checkAtEnd();
      elems = parseUntil(layout.groupKind() == Doc.GroupKind.VERTICAL ? 0 : -1,
          layout, END_COMMAND);
    } else {
      tokens.checkAtEnd();
    }

    // Add snippet definition.
    Location location = Location.create(input.getPath(), input.lineNo);
    String fullName = snippetKind == SnippetKind.PRIVATE ?
        Context.makePrivateSnippetName(location, name) : name;
    Snippet old = snippetSet.get(fullName, paramList.size());
    Snippet snippet = Snippet.create(location, fullName,
        snippetKind, old, layout, paramList, elems);
    snippetSet.add(snippet);
    if (snippetKind == SnippetKind.OVERRIDE && old == null) {
      error("no previous snippet '%s' to override", snippet.displayName());
    }
    if (old != null) {
      if (snippetKind != SnippetKind.OVERRIDE) {
        error("must use '@override name(...) ...' to override snippet '%s'",
            old.displayName());
      } else if (old.location().inputName().equals(snippet.location().inputName())) {
        error("cannot override snippet '%s' defined in the same source at line %s",
            old.displayName(), old.location().lineNo());
      } else if (!old.location().inputName().startsWith(snippet.location().inputName())) {
        error("snippet '%s' cannot be overridden from here since it is not in the extension "
            + "path.%n Current path: %s%n Original path: %s, line %s",
            snippet.displayName(), snippet.location().inputName(), old.location().inputName(),
            old.location().lineNo());
      }
    }
  }

  /**
   * Parse a layout specification.
   */
  private Layout parseLayout(TokenStream tokens) {
    Doc.GroupKind kind = Doc.GroupKind.VERTICAL;
    Doc separator = Doc.BREAK;
    int nest = 0;

    if (tokens.has("vertical")) {
      tokens.next();
      kind = Doc.GroupKind.VERTICAL;
    } else if (tokens.has("horizontal")) {
      tokens.next();
      kind = Doc.GroupKind.HORIZONTAL;
    } else if (tokens.has("auto")) {
      tokens.next();
      kind = Doc.GroupKind.AUTO;
    } else if (tokens.has("fill")) {
      tokens.next();
      kind = Doc.GroupKind.FILL;
    }
    if (tokens.has(INT_LITERAL)) {
      nest = Integer.parseInt(tokens.next());
    }

    if (tokens.has(JOIN_SEPARATOR)) {
      tokens.next();
      Elem expr = parseExpr(tokens);
      if (expr != null) {
        separator = evalParsingTime(expr);
      }
    }
    return Layout.create(separator, kind, nest);
  }

  /**
   * Eval an element at parsing time, based solely on the builtin context.
   */
  private Doc evalParsingTime(Elem elem) {
    try {
      return Values.convertToDoc(elem.eval(new Context(ImmutableMap.of())));
    } catch (EvalException e) {
      error("parsing time evaluation error: %s", e.getMessage());
      return Doc.BREAK;
    }
  }

  /**
   * Trims a literal, removing enclosing {@code "} delimiters if present.
   */
  private String trimLiteral(String lit) {
    if (lit.startsWith("\"")) {
      lit = lit.substring(1);
    }
    if (lit.endsWith("\"")) {
      lit = lit.substring(0, lit.length() - 1);
    }
    return lit;
  }

  /**
   * De-escapes a literal. The only escaped character processed is the double quotation mark.
   */
  private String deEscapeLiteral(String lit) {
    return lit.replaceAll("\\\\\"", "\"");
  }

  /**
   * Parses lines for elements until one of the terminator commands is hit. This will leave
   * the found terminator and terminator parameter in {@link #lastTerminator} and
   * {@link #lastTerminatorHeader}, respectively. Uses the indentation of the first line in
   * sequence to trim indentation of remaining lines. Thus if we have:
   * 
   *   @someuntil
   *     line1
   *       line2
   *    line3
   * 
* .. all lines lines after line1 will have trimmed indentation by 2 or less (line3 will have * only trimmed indentation by 1 because it has less then 2 spaces to work on). */ private List parseUntil(int indent, Layout layout, String... terminators) { lastTerminator = null; lastTerminatorHeader = null; List elems = Lists.newArrayList(); String line; int firstLineIndent = -1; boolean firstContent = true; while ((line = getNextLine()) != null) { int lineIndent = findIndent(line); // Remember the first lines indentation, as it is an anchor for the remaining lines. if (firstLineIndent < 0 && lineIndent < line.length()) { firstLineIndent = lineIndent; } // Compute the effective indent based on block indent and first line's indent, and trim // the line. Indent < 0 here means horizontal or auto mode where we trim all indent. int effectiveIndent = indent < 0 ? indent : indent + (lineIndent - firstLineIndent); if (lineIndent > effectiveIndent) { // Trim superfluous indent. if (effectiveIndent < 0) { line = line.substring(lineIndent); } else { line = line.substring(lineIndent - effectiveIndent); } } // Check for command. Matcher matcher = COMMAND_LINE.matcher(line); if (matcher.find()) { String command = matcher.group(COMMAND_NAME_GROUP); String rest = line.substring(matcher.end()); // Check whether command is one of the expected terminators. for (String terminator : terminators) { if (terminator.equals(command)) { // Remember the seen terminator, as well as the rest of its line. lastTerminator = terminator; lastTerminatorHeader = rest; return elems; } } // Check for other commands switch (command) { case IF_COMMAND: parseIf(effectiveIndent, firstContent, rest, layout, elems); break; case GROUP_COMMAND: parseGroup(effectiveIndent, firstContent, rest, layout, elems); break; case JOIN_COMMAND: parseJoin(effectiveIndent, firstContent, rest, layout, elems); break; case LET_COMMAND: parseLet(effectiveIndent, firstContent, rest, layout, elems); break; case SWITCH_COMMAND: parseSwitch(effectiveIndent, firstContent, rest, layout, elems); break; default: unexpectedCommandError(command); } } else { // Parse the line as content. if (firstContent) { firstContent = false; } else { elems.add(Lit.create(input.location(), layout.separator())); } parseLine(elems, line); } } error("missing '@end' terminator"); return elems; } private int findIndent(String line) { Matcher matcher = FIND_INDENT.matcher(line); matcher.find(); return matcher.group().length(); } /** * Checks whether a header (rest of a command line) is empty and report error if not. */ private void checkHeaderEmpty(String command, String header) { if (!Strings.isNullOrEmpty(header)) { error("command '@%s' has unexpected arguments", command); } } /** * Parse an if command. */ private void parseIf(int indent, boolean firstContent, String header, Layout layout, List elems) { TokenStream tokens = new TokenStream(header); Elem cond = parseExpr(tokens); tokens.checkAtEnd(); List thenElems = parseUntil(indent, layout, END_COMMAND, ELSE_COMMAND); List elseElems = null; if (ELSE_COMMAND.equals(lastTerminator)) { elseElems = parseUntil(indent, layout, END_COMMAND); } if (cond != null && thenElems != null) { elems.add( Block.create(!firstContent, Cond.create(input.location(), cond, thenElems, elseElems))); } } /** * Parse a group command. */ private void parseGroup(int indent, boolean firstContent, String header, @SuppressWarnings("unused") Layout layout, List elems) { TokenStream tokens = new TokenStream(header); Layout groupLayout = parseLayout(tokens); tokens.checkAtEnd(); List bodyElems = parseUntil(indent, groupLayout, END_COMMAND); if (bodyElems != null && groupLayout != null) { elems.add( Block.create(!firstContent, Elem.Group.create(input.location(), groupLayout, bodyElems))); } } /** * Parse a join command. */ private void parseJoin(int indent, boolean firstContent, String header, Layout layout, List elems) { TokenStream tokens = new TokenStream(header); String var = tokens.expect(IDENTIFIER); tokens.expect(":"); Elem generator = parseExpr(tokens); Elem cond = null; if (tokens.has("if")) { tokens.next(); cond = parseExpr(tokens); } Layout joinLayout = parseLayout(tokens); tokens.checkAtEnd(); List bodyElems = parseUntil(indent, layout, END_COMMAND); if (var != null && generator != null && bodyElems != null && layout != null) { elems.add( Block.create(!firstContent, Elem.Join.create(input.location(), var, generator, cond, joinLayout, bodyElems))); } } /** * Parse a let command. */ private void parseLet(int indent, boolean firstContent, String header, Layout layout, List elems) { TokenStream tokens = new TokenStream(header); Elem let = parseLetBindingsThenBody(indent, firstContent, tokens, layout); if (let != null) { elems.add(Block.create(!firstContent, let)); } } /** * Recursively parse bindings and let body. The form let x = e, y = d ... end * is reduced to let x = e let y = d ... end end. */ private Elem parseLetBindingsThenBody(int indent, boolean firstContent, TokenStream tokens, Layout layout) { String var = tokens.expect(IDENTIFIER); tokens.expect("="); Elem value = parseExpr(tokens); List bodyElems; if (tokens.has(",")) { // Parse more bindings. tokens.next(); Elem let = parseLetBindingsThenBody(indent, firstContent, tokens, layout); bodyElems = let != null ? ImmutableList.of(let) : ImmutableList.of(); } else { // End of bindings. tokens.checkAtEnd(); bodyElems = parseUntil(indent, layout, END_COMMAND); } if (var != null && value != null && bodyElems != null) { return Elem.Let.create(input.location(), var, value, bodyElems); } return null; } /** * Parse a switch command. */ private void parseSwitch(int indent, boolean firstContent, String header, Layout layout, List elems) { TokenStream tokens = new TokenStream(header); Elem selector = parseExpr(tokens); tokens.checkAtEnd(); ImmutableList.Builder cases = ImmutableList.builder(); List defaultElems = null; boolean done; String line = getNextLine(); Matcher matcher = COMMAND_LINE.matcher(line); String command; String rest; if (matcher.find()) { command = matcher.group(COMMAND_NAME_GROUP); rest = line.substring(matcher.end()); done = false; } else { error("expected '@end', '@case' or '@default' command after 'switch'"); done = true; command = null; rest = null; } while (!done) { switch (command) { case END_COMMAND: checkHeaderEmpty(command, rest); done = true; break; case DEFAULT_COMMAND: checkHeaderEmpty(command, rest); if (defaultElems != null) { error("duplicate '@default' in @switch"); } defaultElems = parseUntil(indent, layout, END_COMMAND); command = lastTerminator; rest = lastTerminatorHeader; done = lastTerminator == null; break; case CASE_COMMAND: tokens = new TokenStream(rest); Elem value = parseExpr(tokens); tokens.checkAtEnd(); List caseElems = parseUntil(indent, layout, END_COMMAND, CASE_COMMAND, DEFAULT_COMMAND); if (value != null) { cases.add(Case.create(value, caseElems)); } command = lastTerminator; rest = lastTerminatorHeader; done = lastTerminator == null; break; default: unexpectedCommandError(command); done = true; break; } } if (selector != null) { elems.add(Block.create(!firstContent, Switch.create(input.location(), selector, cases.build(), defaultElems))); } } /** * Parse a line into elements. */ private void parseLine(List elems, String line) { int i = 0; boolean lastWasOpenBrace = false; StringBuilder sb = new StringBuilder(); while (i < line.length()) { char ch = line.charAt(i++); if (ch == '@') { if (i < line.length() && line.charAt(i) == '@') { // Looking at @@ escape of @ sb.append('@'); i++; } else if (i < line.length() && line.charAt(i) == '\\') { // Looking at @\ escape of \ sb.append('\\'); i++; } else if (i < line.length() && line.charAt(i) == '#') { // Looking at @# escape of # sb.append('#'); i++; } else if (lastWasOpenBrace) { // Looking at embedded expression {@... sb.deleteCharAt(sb.length() - 1); flushLiteral(elems, sb); i = parseExpr(elems, line, i); } else { sb.append('@'); i++; } lastWasOpenBrace = false; } else { lastWasOpenBrace = ch == '{'; sb.append(ch); } } flushLiteral(elems, sb); } /** * Flushes a literal read so far. */ private void flushLiteral(List elems, StringBuilder sb) { if (sb.length() > 0) { elems.add(Lit.create(input.location(), Doc.text(sb.toString()))); sb.delete(0, sb.length()); } } /** * Parses a expression. */ private int parseExpr(List elems, String line, int i) { int braceLevel = 1; StringBuilder sb = new StringBuilder(); while (i < line.length()) { char ch = line.charAt(i++); switch (ch) { case '{': sb.append('{'); braceLevel++; break; case '}': if (--braceLevel == 0) { TokenStream tokens = new TokenStream(sb.toString()); Elem elem = parseExpr(tokens); tokens.checkAtEnd(); if (elem != null) { elems.add(elem); } return i; } // Fall through default: sb.append(ch); break; } } new TokenStream(sb.toString()).syntaxError("expected '}' to close expression"); return i; } /** * Parse an expression. */ private Elem parseExpr(TokenStream tokens) { Elem expr = parsePrimaryExpr(tokens); Operator.Kind operator = parseOperator(tokens); if (operator != null) { Elem right = parsePrimaryExpr(tokens); if (expr != null && right != null) { expr = Elem.Operator.create(input.location(), operator, expr, right); } } return expr; } /** * Check for and get an operator kind. */ @Nullable private Operator.Kind parseOperator(TokenStream tokens) { if (tokens.has("=")) { tokens.next(); tokens.expect("="); return Operator.Kind.EQUALS; } if (tokens.has("!")) { tokens.next(); tokens.expect("="); return Operator.Kind.NOT_EQUALS; } if (tokens.has("<")) { tokens.next(); if (tokens.has("=")) { tokens.next(); return Operator.Kind.LESS_EQUAL; } return Operator.Kind.LESS; } if (tokens.has(">")) { tokens.next(); if (tokens.has("=")) { tokens.next(); return Operator.Kind.GREATER_EQUAL; } return Operator.Kind.GREATER; } return null; } /** * Parse a primary expression. */ private Elem parsePrimaryExpr(TokenStream tokens) { Elem expr = null; if (tokens.has(LITERAL)) { String lit = tokens.next(); expr = Elem.Lit.create(input.location(), Doc.text(deEscapeLiteral(trimLiteral(lit)))); } else if (tokens.has(IDENTIFIER)) { String var = tokens.expect(IDENTIFIER); if (tokens.has("(")) { // Snippet call: name(arg1, ...) List args = parseOptionalArgs(tokens); if (var != null) { expr = Elem.Call.create(input.location(), var, args); } } else if (var != null) { expr = Elem.Ref.create(input.location(), var); } } else { tokens.syntaxError("expected identifier or literal"); } // Repeated selection while (tokens.has(".")) { tokens.next(); String member = tokens.expect(IDENTIFIER); List args = parseOptionalArgs(tokens); if (member != null && expr != null) { expr = Elem.Reflect.create(input.location(), expr, member, args); } } return expr; } /** * Parse an optional list of arguments. */ private List parseOptionalArgs(TokenStream tokens) { ImmutableList.Builder args = ImmutableList.builder(); if (tokens.has("(")) { tokens.next(); if (!tokens.has(")")) { for (;;) { Elem arg = parseExpr(tokens); if (arg != null) { args.add(arg); } if (tokens.has(",")) { tokens.next(); } else { break; } } } tokens.expect(")"); } return args.build(); } /** * Emit error for an unexpected command. */ private void unexpectedCommandError(String command) { error("command '@%s' is unexpected in this context", command); } /** * Emit error. */ private void error(String message, Object...args) { if (input == null) { errors.add(Issue.create(Location.TOP_LEVEL, message, args)); } else { errors.add(Issue.create(input.location(), message, args)); } } /** * Get the next line from the input, skipping comments, and merging multiple lines * separated by '\'. */ private String getNextLine() { String result = ""; while (input.lines.hasNext()) { String line = input.lines.next(); input.lineNo++; if (SKIP_LINE.matcher(line).matches()) { continue; } line = CharMatcher.whitespace().trimTrailingFrom(line); if (!Strings.isNullOrEmpty(result)) { // If appended to previous line, trim leading space. line = CharMatcher.whitespace().trimLeadingFrom(line); } if (line.endsWith("\\") && !line.endsWith("@\\")) { line = line.substring(0, line.length() - 1); result += line; } else { return result + line; } } if (input.parent != null) { // Close this input and return to the outer one. input = input.parent; return getNextLine(); } return null; } /** * A class representing a token stream. */ private class TokenStream { private final Matcher matcher; private final String expr; private boolean matched; private boolean hasSynErrors; private TokenStream(String expr) { this.expr = expr.trim(); this.matcher = EXPR_TOKEN.matcher(expr); this.matched = this.matcher.find(); } /** * Get the next token from the stream. */ private String next() { String current = matcher.group(EXPR_TOKEN_GROUP); matched = matcher.find(); return current; } /** * Check whether the next token matches the spec, which can either be a string or * a pattern. */ private boolean has(Object spec) { if (!matched) { return false; } String match = matcher.group(EXPR_TOKEN_GROUP); if (spec instanceof Pattern) { return ((Pattern) spec).matcher(match).matches(); } return spec.equals(match); } /** * Expect the specified token, and move next. */ private String expect(Object spec) { if (!has(spec)) { syntaxError(String.format("expected '%s' looking at: ", spec)); return null; } return next(); } /** * Check whether all tokens have been consumed, and report error if not. */ private void checkAtEnd() { if (!hasSynErrors && matched) { syntaxError("unrecognized input: "); } } /** * Report an error. */ private void syntaxError(String message) { hasSynErrors = true; int at = matched ? matcher.start(EXPR_TOKEN_GROUP) : expr.length(); error("%s%n %s%n %s", message.trim(), expr, Strings.padStart("^", at + 1, ' ')); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy