/*******************************************************************************
* Copyright (c) 2015 QNX Software Systems and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* QNX Software Systems - Initial API and implementation
*******************************************************************************/
package org.eclipse.cdt.qt.core.qmldir;
import java.io.InputStream;
import java.util.Stack;
import org.eclipse.cdt.internal.qt.core.location.SourceLocation;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirAST;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirASTNode;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirClassnameCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirCommentCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirDependsCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirDesignerSupportedCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirInternalCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirModuleCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirPluginCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirResourceCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirSingletonCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirSyntaxError;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirTypeInfoCommand;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirVersion;
import org.eclipse.cdt.internal.qt.core.qmldir.QDirWord;
import org.eclipse.cdt.qt.core.qmldir.QMLDirectoryLexer.Token;
import org.eclipse.cdt.qt.core.qmldir.QMLDirectoryLexer.TokenType;
/**
* Converts an <code>InputStream</code> representing a qmldir file into an Abstract Syntax Tree. Uses the {@link QMLDirectoryLexer}
* under the hood to match tokens which it then uses to construct the AST. Also, a <code>QMLDirectoryParser</code> has the ability
* to skip over syntax errors and include them in its AST rather than returning upon the first error.
*/
public class QMLDirectoryParser {
/**
* An exception thrown when a <code>QMLDirectoryParser</code> encounters a syntax error. This class stores information on the
* offending token as well as the node the parser was working on before it failed (if available).
*/
public static class SyntaxError extends RuntimeException {
private static final long serialVersionUID = 6608815552297970623L;
private final IQDirASTNode incompleteNode;
private final Token offendingToken;
/**
* Creates a new <code>SyntaxError</code>.
*
* @param node
* the incomplete working node
* @param token
* the offending token
* @param message
* the message to display
*/
public SyntaxError(IQDirASTNode node, Token token, String message) {
super(message);
this.incompleteNode = node;
this.offendingToken = token;
}
/**
* Gets the token that caused the parser to fail.
*
* @return the offending token
*/
public Token getOffendingToken() {
return offendingToken;
}
/**
* Gets the last node that the parser was working on before it failed or null if that information isn't present.
*
* @return the incomplete node or <code>null</code> if not available
*/
public IQDirASTNode getIncompleteNode() {
return incompleteNode;
}
}
private final QMLDirectoryLexer lexer;
private final Stack<QDirASTNode> workingNodes;
private Token tok;
/**
* Initializes a new <code>QMLDirectoryParser</code> capable of parsing an <code>InputStream</code> and returning an AST.
*/
public QMLDirectoryParser() {
this.lexer = new QMLDirectoryLexer();
this.workingNodes = new Stack<>();
}
/**
* Parses the given <code>InputStream</code> into an Abstract Syntax Tree. This is a helper method equivalent to
* <code>parse(input, true)</code>. That is, the parser will attempt to recover once it hits an error and include an
* {@link IQDirSyntaxError} node in the AST.
*
* @param input
* the input to parse
* @return the Abstract Syntax Tree representing the input
* @see QMLDirectoryParser#parse(InputStream, boolean)
*/
public IQDirAST parse(InputStream input) {
return parse(input, true);
}
/**
* Parses the given <code>InputStream</code> into an Abstract Syntax Tree. If <code>tolerateErrors</code> is <code>true</code>,
* any syntax errors will be included in the AST as a separate {@link IQDirSyntaxErrorCommand}. The parser will then attempt to
* recover by jumping to the next line and continue parsing. A value of </code>false</code> tells the parser to throw a
* {@link SyntaxError} on the first problem it encounters.
*
* @param input
* the input to parse
* @param tolerateErrors
* whether or not the parser should be error tolerant
* @return the Abstract Syntax Tree representing the input
*/
public IQDirAST parse(InputStream input, boolean tolerateErrors) {
// Clear out any leftover state
this.lexer.setInput(input);
this.workingNodes.clear();
QDirAST ast = new QDirAST();
nextToken();
while (tok.getType() != TokenType.EOF) {
try {
switch (tok.getType()) {
case MODULE:
ast.addCommand(parseModuleCommand());
break;
case SINGLETON:
ast.addCommand(parseSingletonCommand());
break;
case INTERNAL:
ast.addCommand(parseInternalCommand());
break;
case WORD:
ast.addCommand(parseResourceCommand());
break;
case PLUGIN:
ast.addCommand(parsePluginCommand());
break;
case CLASSNAME:
ast.addCommand(parseClassnameCommand());
break;
case TYPEINFO:
ast.addCommand(parseTypeInfoCommand());
break;
case DEPENDS:
ast.addCommand(parseDependsCommand());
break;
case DESIGNERSUPPORTED:
ast.addCommand(parseDesignerSupportedCommand());
break;
case COMMENT:
ast.addCommand(parseCommentCommand());
break;
case COMMAND_END:
// This is just an empty line that should be ignored
nextToken();
break;
default:
throw unexpectedToken();
}
} catch (SyntaxError e) {
if (!tolerateErrors) {
throw e;
}
// Add the syntax error to the AST and jump to the next line
QDirSyntaxError errNode = new QDirSyntaxError(e);
markStart(errNode);
IQDirASTNode node = e.getIncompleteNode();
if (node != null) {
errNode.setLocation((SourceLocation) node.getLocation());
errNode.setStart(node.getStart());
errNode.setEnd(node.getEnd());
}
while (!eat(TokenType.COMMAND_END) && !eat(TokenType.EOF)) {
nextToken();
}
markEnd();
ast.addCommand(errNode);
}
}
return ast;
}
private void nextToken() {
nextToken(true);
}
private void nextToken(boolean skipWhitespace) {
tok = lexer.nextToken(skipWhitespace);
}
private void markStart(QDirASTNode node) {
workingNodes.push(node);
node.setStart(tok.getStart());
node.setLocation(new SourceLocation());
node.getLocation().setStart(tok.getLocation().getStart());
}
private void markEnd() {
QDirASTNode node = workingNodes.pop();
node.setEnd(tok.getEnd());
node.getLocation().setEnd(tok.getLocation().getEnd());
}
private boolean eat(TokenType type) {
if (tok.getType() == type) {
nextToken();
return true;
}
return false;
}
private SyntaxError syntaxError(String message) {
return new SyntaxError(workingNodes.peek(), tok, message + " " + tok.getLocation().getStart().toString()); //$NON-NLS-1$
}
private SyntaxError unexpectedToken() {
String tokenText = tok.getText();
if (tok.getType() == TokenType.EOF) {
tokenText = "EOF"; //$NON-NLS-1$
}
return syntaxError("Unexpected token '" + tokenText + "'"); //$NON-NLS-1$ //$NON-NLS-2$
}
private void expect(TokenType type) {
if (tok.getType() != type) {
throw unexpectedToken();
}
nextToken();
}
private void expectCommandEnd() {
// Allow EOF to be substituted for COMMAND_END
if (tok.getType() == TokenType.EOF) {
nextToken();
return;
}
if (tok.getType() != TokenType.COMMAND_END) {
throw syntaxError("Expected token '\\n' or 'EOF', but saw '" + tok.getText() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
}
nextToken();
}
private QDirModuleCommand parseModuleCommand() {
QDirModuleCommand node = new QDirModuleCommand();
markStart(node);
expect(TokenType.MODULE);
if (tok.getType() == TokenType.WORD) {
node.setModuleIdentifier(parseWord());
expectCommandEnd();
markEnd();
return node;
}
throw unexpectedToken();
}
private QDirSingletonCommand parseSingletonCommand() {
QDirSingletonCommand node = new QDirSingletonCommand();
markStart(node);
expect(TokenType.SINGLETON);
if (tok.getType() == TokenType.WORD) {
node.setTypeName(parseWord());
if (tok.getType() == TokenType.DECIMAL) {
node.setInitialVersion(parseVersion());
if (tok.getType() == TokenType.WORD) {
node.setFile(parseWord());
expectCommandEnd();
markEnd();
return node;
}
}
}
throw unexpectedToken();
};
private QDirInternalCommand parseInternalCommand() {
QDirInternalCommand node = new QDirInternalCommand();
markStart(node);
expect(TokenType.INTERNAL);
if (tok.getType() == TokenType.WORD) {
node.setTypeName(parseWord());
if (tok.getType() == TokenType.WORD) {
node.setFile(parseWord());
expectCommandEnd();
markEnd();
return node;
}
}
throw unexpectedToken();
}
private QDirResourceCommand parseResourceCommand() {
QDirResourceCommand node = new QDirResourceCommand();
markStart(node);
if (tok.getType() == TokenType.WORD) {
node.setResourceIdentifier(parseWord());
if (tok.getType() == TokenType.DECIMAL) {
node.setInitialVersion(parseVersion());
if (tok.getType() == TokenType.WORD) {
node.setFile(parseWord());
expectCommandEnd();
markEnd();
return node;
}
}
}
throw unexpectedToken();
}
private QDirPluginCommand parsePluginCommand() {
QDirPluginCommand node = new QDirPluginCommand();
markStart(node);
expect(TokenType.PLUGIN);
if (tok.getType() == TokenType.WORD) {
node.setName(parseWord());
if (tok.getType() == TokenType.WORD) {
node.setPath(parseWord());
}
expectCommandEnd();
markEnd();
return node;
}
throw unexpectedToken();
}
private QDirClassnameCommand parseClassnameCommand() {
QDirClassnameCommand node = new QDirClassnameCommand();
markStart(node);
expect(TokenType.CLASSNAME);
if (tok.getType() == TokenType.WORD) {
node.setIdentifier(parseWord());
expectCommandEnd();
markEnd();
return node;
}
throw unexpectedToken();
}
private QDirTypeInfoCommand parseTypeInfoCommand() {
QDirTypeInfoCommand node = new QDirTypeInfoCommand();
markStart(node);
expect(TokenType.TYPEINFO);
if (tok.getType() == TokenType.WORD) {
node.setFile(parseWord());
expectCommandEnd();
markEnd();
return node;
}
throw unexpectedToken();
}
private QDirDependsCommand parseDependsCommand() {
QDirDependsCommand node = new QDirDependsCommand();
markStart(node);
expect(TokenType.DEPENDS);
if (tok.getType() == TokenType.WORD) {
node.setModuleIdentifier(parseWord());
if (tok.getType() == TokenType.DECIMAL) {
node.setInitialVersion(parseVersion());
expectCommandEnd();
markEnd();
return node;
}
}
throw unexpectedToken();
}
private QDirDesignerSupportedCommand parseDesignerSupportedCommand() {
QDirDesignerSupportedCommand node = new QDirDesignerSupportedCommand();
markStart(node);
expect(TokenType.DESIGNERSUPPORTED);
expectCommandEnd();
markEnd();
return node;
}
private QDirCommentCommand parseCommentCommand() {
QDirCommentCommand node = new QDirCommentCommand();
markStart(node);
if (tok.getType() == TokenType.COMMENT) {
node.setText(tok.getText());
nextToken();
expectCommandEnd();
markEnd();
return node;
}
throw unexpectedToken();
}
private QDirVersion parseVersion() {
QDirVersion node = new QDirVersion();
markStart(node);
if (tok.getType() == TokenType.DECIMAL) {
node.setVersionString(tok.getText());
nextToken();
markEnd();
return node;
}
throw unexpectedToken();
}
private QDirWord parseWord() {
QDirWord node = new QDirWord();
markStart(node);
if (tok.getType() == TokenType.WORD) {
node.setText(tok.getText());
nextToken();
markEnd();
return node;
}
throw unexpectedToken();
}
}