/*******************************************************************************
* Copyright (c) 2007, 2016 Red Hat, Inc. 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:
* Red Hat Incorporated - initial API and implementation
* Ed Swartz (Nokia) - refactoring
*******************************************************************************/
package org.eclipse.cdt.autotools.ui.editors.parser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.cdt.autotools.ui.editors.AcInitElement;
import org.eclipse.cdt.autotools.ui.editors.AutoconfEditorMessages;
import org.eclipse.core.resources.IMarker;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
/**
* Tokenizing autoconf parser, based on original work by Jeff Johnston
* @author eswartz
*/
public class AutoconfParser {
public static final String MISSING_SPECIFIER = "MissingSpecifier"; //$NON-NLS-1$
public static final String INVALID_SPECIFIER = "InvalidSpecifier"; //$NON-NLS-1$
public static final String INVALID_TERMINATION = "InvalidTermination"; //$NON-NLS-1$
public static final String UNTERMINATED_CONSTRUCT = "UnterminatedConstruct"; //$NON-NLS-1$
public static final String MISSING_CONDITION = "MissingCondition"; //$NON-NLS-1$
public static final String INVALID_ELIF = "InvalidElif"; //$NON-NLS-1$
public static final String INVALID_ELSE = "InvalidElse"; //$NON-NLS-1$
public static final String INVALID_FI = "InvalidFi"; //$NON-NLS-1$
public static final String INVALID_DONE = "InvalidDone"; //$NON-NLS-1$
public static final String INVALID_ESAC = "InvalidEsac"; //$NON-NLS-1$
public static final String INVALID_DO = "InvalidDo"; //$NON-NLS-1$
public static final String INVALID_THEN = "InvalidThen"; //$NON-NLS-1$
public static final String INVALID_IN = "InvalidIn"; //$NON-NLS-1$
public static final String IMPROPER_CASE_CONDITION = "ImproperCaseCondition"; //$NON-NLS-1$
public static final String UNTERMINATED_CASE_CONDITION = "UnterminatedCaseCondition"; //$NON-NLS-1$
public static final String UNTERMINATED_INLINE_DOCUMENT = "UnterminatedInlineDocument"; //$NON-NLS-1$
public static final String INCOMPLETE_INLINE_MARKER="IncompleteInlineMarker"; //$NON-NLS-1$
public static final String MISSING_INLINE_MARKER="MissingInlineMarker"; //$NON-NLS-1$
public static final String UNMATCHED_RIGHT_PARENTHESIS = "UnmatchedRightParenthesis"; //$NON-NLS-1$
public static final String UNMATCHED_LEFT_PARENTHESIS = "UnmatchedLeftParenthesis"; //$NON-NLS-1$
private IAutoconfErrorHandler errorHandler;
private IAutoconfMacroValidator macroValidator;
private AutoconfTokenizer tokenizer;
private IAutoconfMacroDetector macroDetector;
private static final String M4_BUILTINS =
"define undefine defn pushdef popdef indir builtin ifdef ifelse shift reverse cond " + //$NON-NLS-1$
"dumpdef traceon traceoff debugmode debugfile dnl changequote changecom changeword " + //$NON-NLS-1$
"m4wrap " + //$NON-NLS-1$
"include sinclude divert undivert divnum len index regexp substr translit patsubst " + //$NON-NLS-1$
"format incr decr eval syscmd esyscmd sysval mkstemp maketemp errprint m4exit " + //$NON-NLS-1$
"__file__ __line__ __program__ "; //$NON-NLS-1$
private static List<String> m4builtins = new ArrayList<>();
static {
m4builtins.addAll(Arrays.asList(M4_BUILTINS.split(" "))); //$NON-NLS-1$
}
/**
* Create a parser for autoconf-style sources.
* @param errorHandler
* @param macroDetector
* @param macroValidator
*/
public AutoconfParser(IAutoconfErrorHandler errorHandler,
IAutoconfMacroDetector macroDetector,
IAutoconfMacroValidator macroValidator) {
this.errorHandler = errorHandler;
this.macroDetector = macroDetector;
this.macroValidator = macroValidator;
}
/**
* Parse the given document and produce an AutoconfElement tree
* @param document
* @return element tree
*/
public AutoconfElement parse(IDocument document) {
return parse(document, true);
}
/**
* Parse the given document and produce an AutoconfElement tree,
* and control whether the initial quoting style is m4 style (`...')
* or autoconf style ([ ... ]).
* @param errorHandler
* @param macroValidator
* @param document
* @param useAutoconfQuotes
* @return element tree
*/
public AutoconfElement parse(IDocument document, boolean useAutoconfQuotes) {
this.tokenizer = new AutoconfTokenizer(document, errorHandler);
if (useAutoconfQuotes)
tokenizer.setM4Quote("[", "]"); //$NON-NLS-1$ //$NON-NLS-2$
AutoconfElement root = new AutoconfRootElement();
Token eof = parseTopLevel(root);
root.setStartOffset(0);
root.setDocument(document);
setSourceEnd(root, eof);
return root;
}
static class BlockEndCondition extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
private Token token;
public BlockEndCondition(Token token) {
this.token = token;
}
public Token getToken() {
return token;
}
}
static class ExprEndCondition extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
private Token token;
public ExprEndCondition(Token token) {
this.token = token;
}
public Token getToken() {
return token;
}
}
/**
* Parse individual top-level nodes: divide text into macro calls
* and recognized shell constructs. Anything else is not accounted for.
* @param parent
* @return
*/
protected Token parseTopLevel(AutoconfElement parent) {
while (true) {
try {
parseStatement(parent);
} catch (BlockEndCondition e) {
// don't terminate here; we may have constructs closed too early
Token token = tokenizer.peekToken();
if (token.getType() == ITokenConstants.EOF)
return token;
}
}
}
/**
* Parse a block of nodes, which starts with an expression and contains
* subnodes. Divide text into macro calls and recognized shell constructs.
* Anything else is not accounted for.
* @param parent
*/
protected void parseBlock(AutoconfElement parent, Token open, AutoconfElement block) throws BlockEndCondition {
parent.addChild(block);
setSourceStart(block, open);
// get the expression part
Token token;
try {
token = parseBlockExpression(open, block);
} catch (BlockEndCondition e) {
setSourceEndBefore(block, e.getToken());
throw e;
}
// parse the block proper
if (token.getType() != ITokenConstants.EOF) {
while (true) {
try {
parseStatement(block);
} catch (BlockEndCondition e) {
setSourceEnd(block, e.getToken());
return;
}
}
} else {
setSourceEnd(block, token);
}
}
private Token parseBlockExpression(Token open, AutoconfElement block) throws BlockEndCondition {
Token token;
try {
if (block instanceof AutoconfIfElement
|| block instanceof AutoconfElifElement
|| block instanceof AutoconfCaseElement
|| block instanceof AutoconfWhileElement) {
token = parseExpression(block);
} else if (block instanceof AutoconfForElement) {
token = parseForExpression(block);
} else {
// no expression
return open;
}
block.setVar(getTokenSpanTextBetween(open, token));
} catch (BlockEndCondition e) {
// oops, premature end
setSourceEnd(block, e.getToken());
throw e;
}
// check for expected token
while (true) {
token = tokenizer.readToken();
if (token.getType() == ITokenConstants.EOF)
break;
if (token.getType() != ITokenConstants.EOL)
break;
}
if (token.getType() == ITokenConstants.SH_DO) {
checkBlockValidity(block, token,
new Class[] { AutoconfForElement.class, AutoconfWhileElement.class },
INVALID_DO);
}
else if (token.getType() == ITokenConstants.SH_THEN) {
checkBlockValidity(block, token,
new Class[] { AutoconfIfElement.class, AutoconfElifElement.class },
INVALID_THEN);
}
else {
String exp;
if (block instanceof AutoconfIfElement || block instanceof AutoconfElifElement)
exp = "then";
else
exp = "do";
handleError(token, AutoconfEditorMessages.getFormattedString(MISSING_SPECIFIER, exp));
// assume we're still in the block...
tokenizer.unreadToken(token);
}
return token;
}
/**
* Parse a case statement. Scoop up statements into case conditional blocks.
* <pre>
* 'case' EXPR 'in'
* { EXPR ')' { STMTS } ';;' }
* 'esac'
* </pre>
* @param parent
* @return
*/
protected void parseCaseBlock(AutoconfElement parent, Token open, AutoconfElement block) throws BlockEndCondition {
parent.addChild(block);
setSourceStart(block, open);
// get the case expression, terminating at 'in'
Token token;
try {
token = parseCaseExpression(block);
} catch (BlockEndCondition e) {
// oops, premature end
setSourceEnd(block, e.getToken());
throw e;
}
block.setVar(getTokenSpanTextBetween(open, token));
// now get the statements, which are themselves blocks... just read statements
// that terminate with ';;' and stuff those into blocks.
while (true) {
AutoconfCaseConditionElement condition = new AutoconfCaseConditionElement();
// skip EOLs and get the first "real" token
while (true) {
token = tokenizer.readToken();
setSourceStart(condition, token);
if (token.getType() == ITokenConstants.EOF)
break;
if (token.getType() == ITokenConstants.EOL)
continue;
break;
}
if (token.getType() == ITokenConstants.SH_ESAC) {
break;
}
try {
Token start = token;
token = parseCaseExpression(condition);
condition.setName(getTokenSpanTextFromUpTo(start, token));
while (true) {
parseStatement(condition);
}
} catch (BlockEndCondition e) {
setSourceEnd(condition, e.getToken());
if (condition.getSource().length() > 0)
block.addChild(condition);
if (e.getToken().getType() != ITokenConstants.SH_CASE_CONDITION_END) {
token = e.getToken();
break;
}
}
}
setSourceEnd(block, token);
if (token.getType() != ITokenConstants.SH_ESAC) {
handleError(token, AutoconfEditorMessages.getFormattedString(UNTERMINATED_CONSTRUCT, block.getName()));
}
}
private String getTokenSpanTextBetween(Token open, Token close) {
int startOffset = open.getOffset() + open.getLength();
int endOffset = close.getOffset();
if (open.getDocument() != close.getDocument())
return open.getText();
String text;
try {
text = open.getDocument().get(startOffset, endOffset - startOffset).trim();
} catch (BadLocationException e) {
text = open.getText();
// TODO: error
}
return text;
}
private String getTokenSpanTextFromUpTo(Token open, Token close) {
int startOffset = open.getOffset();
int endOffset = close.getOffset();
if (open.getDocument() != close.getDocument())
return open.getText();
String text;
try {
text = open.getDocument().get(startOffset, endOffset - startOffset).trim();
} catch (BadLocationException e) {
text = open.getText();
// TODO: error
}
return text;
}
private void setSourceStart(AutoconfElement block, Token open) {
int offset = open.getOffset();
block.setDocument(open.getDocument());
block.setStartOffset(offset);
}
private void setSourceEnd(AutoconfElement block, Token close) {
int offset = close.getOffset() + close.getLength();
if (block.getDocument() != null && block.getDocument() != close.getDocument())
throw new IllegalStateException();
block.setDocument(close.getDocument());
block.setEndOffset(offset);
}
private void setSourceEndBefore(AutoconfElement block, Token close) {
int offset = close.getOffset();
if (block.getDocument() != null && block.getDocument() != close.getDocument())
throw new IllegalStateException();
block.setDocument(close.getDocument());
block.setEndOffset(offset);
}
/**
* Parse a single statement (macro call or a shell construct).
* This can recursively invoke parseBlock() or parseStatement() to populate the tree.
* Whenever a token terminates a block, we check its validity and then throw BlockEndCondition.
* @param parent the parent into which to add statements. The type of this element is used
* to validate the legality of block closing tokens.
*/
protected void parseStatement(AutoconfElement parent) throws BlockEndCondition {
boolean atStart = true;
while (true) {
Token token = tokenizer.readToken();
switch (token.getType()) {
// 0. Check EOF
case ITokenConstants.EOF:
AutoconfElement element = parent;
while (element != null && !(element instanceof AutoconfRootElement)) {
handleError(token, AutoconfEditorMessages.getFormattedString(UNTERMINATED_CONSTRUCT, element.getName()));
element = element.getParent();
}
throw new BlockEndCondition(token);
// 1. Check for end of statement
case ITokenConstants.EOL:
case ITokenConstants.SEMI:
return;
// 2. Check macro expansions
case ITokenConstants.WORD:
checkMacro(parent, token);
atStart = false;
break;
// Check for shell constructs. These should appear at the start of a line
// or after a semicolon. If they don't, just report an error and continue,
// to be tolerant of our own lax parsing.
// 3.a) Check dollar variables
case ITokenConstants.SH_DOLLAR:
// skip the next token
atStart = false;
token = tokenizer.readToken();
continue;
// 3.b) Look for if/else/elif/fi constructs,
// being tolerant of nesting problems by allowing
// stranded else/elif nodes but reporting errors.
case ITokenConstants.SH_IF:
checkLineStart(token, atStart);
parseBlock(parent, token, new AutoconfIfElement());
break;
case ITokenConstants.SH_ELIF:
checkLineStart(token, atStart);
checkBlockValidity(
parent, token,
new Class[] { AutoconfIfElement.class, AutoconfElifElement.class },
INVALID_ELIF);
parseBlock(parent, token, new AutoconfElifElement());
token = tokenizer.peekToken();
throw new BlockEndCondition(token);
case ITokenConstants.SH_ELSE:
checkLineStart(token, atStart);
checkBlockValidity(
parent, token,
new Class[] { AutoconfIfElement.class, AutoconfElifElement.class },
INVALID_ELSE);
parseBlock(parent, token, new AutoconfElseElement());
token = tokenizer.peekToken();
throw new BlockEndCondition(token);
case ITokenConstants.SH_FI:
checkLineStart(token, atStart);
checkBlockValidity(
parent, token,
new Class[] { AutoconfIfElement.class, AutoconfElifElement.class, AutoconfElseElement.class },
INVALID_FI);
throw new BlockEndCondition(token);
// 4. Look for for/while loops
case ITokenConstants.SH_FOR:
checkLineStart(token, atStart);
parseBlock(parent, token, new AutoconfForElement());
break;
case ITokenConstants.SH_WHILE:
checkLineStart(token, atStart);
parseBlock(parent, token, new AutoconfWhileElement());
break;
case ITokenConstants.SH_UNTIL:
checkLineStart(token, atStart);
parseBlock(parent, token, new AutoconfUntilElement());
break;
case ITokenConstants.SH_SELECT:
checkLineStart(token, atStart);
parseBlock(parent, token, new AutoconfSelectElement());
break;
case ITokenConstants.SH_DONE:
checkLineStart(token, atStart);
checkBlockValidity(
parent, token,
new Class[] { AutoconfForElement.class, AutoconfWhileElement.class,
AutoconfUntilElement.class, AutoconfSelectElement.class },
INVALID_DONE);
throw new BlockEndCondition(token);
// 5. Look for case statements
case ITokenConstants.SH_CASE:
checkLineStart(token, atStart);
parseCaseBlock(parent, token, new AutoconfCaseElement());
break;
case ITokenConstants.SH_CASE_CONDITION_END:
checkBlockValidity(
parent, token,
new Class[] { AutoconfCaseConditionElement.class },
IMPROPER_CASE_CONDITION);
throw new BlockEndCondition(token);
case ITokenConstants.SH_ESAC:
checkLineStart(token, atStart);
checkBlockValidity(
parent, token,
// note: we don't strictly recurse here, so accept either parent
new Class[] { AutoconfCaseElement.class, AutoconfCaseConditionElement.class },
INVALID_ESAC);
throw new BlockEndCondition(token);
// 6. Check for HERE documents
case ITokenConstants.SH_HERE:
case ITokenConstants.SH_HERE_DASH:
parseHERE(parent, token);
break;
}
}
}
private void checkLineStart(Token token, boolean atStart) {
if (!atStart) {
handleError(token, AutoconfEditorMessages.getFormattedString(INVALID_TERMINATION, token.getText()));
}
}
/**
* Parse the Here document, whose control token is provided (SH_HERE or SH_HERE_DASH).
* Contents are thrown away except for any macro calls.
* @param parent
* @param controlToken
*/
private void parseHERE(AutoconfElement parent, Token controlToken) {
Token token = tokenizer.readToken();
if (token.getType() == ITokenConstants.EOL || token.getType() == ITokenConstants.EOF) {
handleError(token, AutoconfEditorMessages.getString(INCOMPLETE_INLINE_MARKER));
} else {
String hereTag = token.getText();
boolean atEOL = false;
while (true) {
token = tokenizer.readToken();
if (token.getType() == ITokenConstants.EOF) {
handleError(token, AutoconfEditorMessages.getFormattedString(UNTERMINATED_CONSTRUCT, parent.getName()));
break;
} else if (token.getType() == ITokenConstants.EOL) {
atEOL = true;
} else {
if (atEOL && token.getText().equals(hereTag)) {
// only the end if it is also followed by EOL without any whitespace
Token eol = tokenizer.readToken();
if (eol.getType() == ITokenConstants.EOL && eol.getOffset() == token.getOffset() + token.getLength()) {
break;
}
}
if (token.getType() == ITokenConstants.WORD) {
checkMacro(parent, token);
}
atEOL = false;
}
}
}
}
/**
* Parse through a single expression up to a semicolon or newline.
* Add a macro call to the element or just return upon finding the desired token.
* Whenever a token terminates the expression, we check its validity and return the final token
* Throw {@link BlockEndCondition} if an unexpected token was found.
* @param parent the parent into which to add statements. The type of this element is used
* to validate the legality of block closing tokens.
*/
protected Token parseExpression(AutoconfElement parent) throws BlockEndCondition {
while (true) {
Token token = tokenizer.readToken();
// 0. Ignore comments (tokenizer skipped them!)
switch (token.getType()) {
// 1. Check EOF
case ITokenConstants.EOF:
throw new BlockEndCondition(token);
// 2. Check macro expansions
case ITokenConstants.WORD:
token = checkMacro(parent, token);
break;
// 3. Check expression terminators
case ITokenConstants.SEMI:
case ITokenConstants.EOL:
return token;
// 4. Handle variables
case ITokenConstants.SH_DOLLAR:
token = tokenizer.readToken();
break;
case ITokenConstants.SH_IN:
// in 'for' or 'select, an 'in' may occur before 'do'
if (!(parent instanceof AutoconfForElement)
&& !(parent instanceof AutoconfSelectElement))
return token;
// fall through
// 5. Abort on unexpected tokens
case ITokenConstants.SH_DO:
case ITokenConstants.SH_THEN:
handleError(token, AutoconfEditorMessages.getFormattedString(INVALID_SPECIFIER, token.getText()));
tokenizer.unreadToken(token);
// close enough...
return token;
case ITokenConstants.SH_ESAC:
case ITokenConstants.SH_CASE:
case ITokenConstants.SH_CASE_CONDITION_END:
case ITokenConstants.SH_FOR:
case ITokenConstants.SH_IF:
case ITokenConstants.SH_ELIF:
case ITokenConstants.SH_ELSE:
case ITokenConstants.SH_FI:
case ITokenConstants.SH_DONE:
handleError(token, AutoconfEditorMessages.getFormattedString(UNTERMINATED_CONSTRUCT, parent.getName()));
tokenizer.unreadToken(token);
throw new BlockEndCondition(token);
}
}
}
/**
* Parse through a single expression up to 'do'.
* Add a macro call to the element or just return upon finding the desired token.
* Whenever a token terminates the expression, we check its validity and then throw ExprEndCondition.
* Throw {@link BlockEndCondition} if an unexpected token was found.
* @param parent the parent into which to add statements. The type of this element is used
* to validate the legality of block closing tokens.
*/
protected Token parseForExpression(AutoconfElement parent) throws BlockEndCondition {
while (true) {
Token token = tokenizer.readToken();
// 0. Ignore comments (tokenizer skipped them!)
// 1. Check EOF
if (token.getType() == ITokenConstants.EOF) {
throw new BlockEndCondition(token);
}
// 2. Check macro expansions
else if (token.getType() == ITokenConstants.WORD) {
token = checkMacro(parent, token);
}
// 3. Check expression terminators -- not ';' here, but 'do'
else if (token.getType() == ITokenConstants.SH_DO) {
tokenizer.unreadToken(token);
return tokenizer.peekToken();
}
// 4. Abort on unexpected tokens
else switch (token.getType()) {
case ITokenConstants.SH_THEN:
handleError(token, AutoconfEditorMessages.getFormattedString(INVALID_SPECIFIER, token.getText()));
tokenizer.unreadToken(token);
// close enough...
//throw new ExprEndCondition(token);
return token;
case ITokenConstants.SH_ESAC:
case ITokenConstants.SH_CASE:
case ITokenConstants.SH_CASE_CONDITION_END:
case ITokenConstants.SH_FOR:
case ITokenConstants.SH_IF:
case ITokenConstants.SH_ELIF:
case ITokenConstants.SH_ELSE:
case ITokenConstants.SH_FI:
case ITokenConstants.SH_DONE:
handleError(token, AutoconfEditorMessages.getFormattedString(UNTERMINATED_CONSTRUCT, parent.getName()));
tokenizer.unreadToken(token);
throw new BlockEndCondition(token);
}
}
}
/**
* Parse through a single expression up to 'in'.
* Add a macro call to the element or just return upon finding the desired token.
* Whenever a token terminates the expression, we check its validity and then throw ExprEndCondition.
* Throw {@link BlockEndCondition} if an unexpected token was found.
* @param parent the parent into which to add statements. The type of this element is used
* to validate the legality of block closing tokens.
*/
protected Token parseCaseExpression(AutoconfElement parent) throws BlockEndCondition {
while (true) {
Token token = tokenizer.readToken();
// 0. Ignore comments (tokenizer skipped them!)
// 1. Check EOF
if (token.getType() == ITokenConstants.EOF) {
throw new BlockEndCondition(token);
}
// 2. Check macro expansions
else if (token.getType() == ITokenConstants.WORD) {
token = checkMacro(parent, token);
}
// 3. Check expression terminators
else if (parent instanceof AutoconfCaseElement && token.getType() == ITokenConstants.SH_IN) {
return token;
}
else if (parent instanceof AutoconfCaseConditionElement && token.getType() == ITokenConstants.RPAREN) {
return token;
}
// 4. Abort on unexpected tokens
else switch (token.getType()) {
case ITokenConstants.SEMI:
case ITokenConstants.SH_IN:
case ITokenConstants.RPAREN:
case ITokenConstants.SH_DO:
case ITokenConstants.SH_THEN:
if (parent instanceof AutoconfCaseElement)
handleError(token, AutoconfEditorMessages.getString(INVALID_IN));
else
handleError(token, AutoconfEditorMessages.getString(IMPROPER_CASE_CONDITION));
// close enough...
return token;
case ITokenConstants.SH_ESAC:
case ITokenConstants.SH_CASE:
case ITokenConstants.SH_CASE_CONDITION_END:
case ITokenConstants.SH_FOR:
case ITokenConstants.SH_IF:
case ITokenConstants.SH_ELIF:
case ITokenConstants.SH_ELSE:
case ITokenConstants.SH_FI:
case ITokenConstants.SH_DONE:
handleError(token, AutoconfEditorMessages.getFormattedString(UNTERMINATED_CONSTRUCT, parent.getName()));
tokenizer.unreadToken(token);
throw new BlockEndCondition(token);
}
}
}
/**
* Check a given close block token against the current parent by checking that
* the parent's class is one of classes, If a match happens,
* optionally push back the token if it will be used to parse new statements in the parent.
* Report an error if the parent is not one of the expected kinds.
* @param parent
* @param token
* @param classes
*/
private void checkBlockValidity(
AutoconfElement parent,
Token token,
Class<?>[] classes,
String errorMessage) {
for (int i = 0; i < classes.length; i++) {
if (classes[i].isInstance(parent)) {
return;
}
}
// not a match
handleError(token, AutoconfEditorMessages.getFormattedString(errorMessage, parent.getName(), token.getText()));
}
/**
* Creates the appropriate macro type object depending on the
* macro name.
*
* @return
*/
private AutoconfMacroElement createMacroElement (String name){
if (name.equals("AC_INIT")){ //$NON-NLS-1$
return new AcInitElement(name);
}
return new AutoconfMacroElement(name);
}
/**
* Check whether the given token is part of a macro, and parse it as such
* if necessary.
* @param parent
* @param token
* @return Token last read for the macro call
*/
private Token checkMacro(AutoconfElement parent, Token token) {
String name = token.getText();
boolean hasArguments = tokenizer.peekToken().getType() == ITokenConstants.LPAREN;
if (macroDetector != null && macroDetector.isMacroIdentifier(name)) {
// ok
} else if (m4builtins.contains(name)) {
// all of these except dnl take arguments
if (!name.equals("dnl") && !hasArguments) //$NON-NLS-1$
return token;
} else {
return token;
}
AutoconfMacroElement macro = createMacroElement(name);
token = parseMacro(macro, token);
// handle special macros here
if ("dnl".equals(name)) { //$NON-NLS-1$
// consume to EOL
while (true) {
token = tokenizer.readToken();
if (token.getType() == ITokenConstants.EOF || token.getType() == ITokenConstants.EOL)
break;
}
// ignore macro entirely
macro = null;
} else if ("changequote".equals(name)) { //$NON-NLS-1$
// change quote delimiters
validateMacroParameterCount(macro, token, 2);
// GNU behavior for invalid argument count
String parm0 = "`"; //$NON-NLS-1$
String parm1 = "'"; //$NON-NLS-1$
if (macro.getParameterCount() >= 1)
parm0 = macro.getParameter(0);
if (macro.getParameterCount() >= 2)
parm1 = macro.getParameter(1);
tokenizer.setM4Quote(parm0, parm1);
} else if ("changecom".equals(name)) { //$NON-NLS-1$
// change comment delimiters
validateMacroParameterCount(macro, token, 2);
// GNU behavior for invalid argument count
String parm0 = "#"; //$NON-NLS-1$
String parm1 = "\n"; //$NON-NLS-1$
if (macro.getParameterCount() >= 1)
parm0 = macro.getParameter(0);
if (macro.getParameterCount() >= 2)
parm1 = macro.getParameter(1);
tokenizer.setM4Comment(parm0, parm1);
}
if (macro != null) {
parent.addChild(macro);
}
// now validate that the macro is properly terminated
if (!(parent instanceof AutoconfMacroArgumentElement)
&& !(parent instanceof AutoconfMacroElement)
&& !(parent instanceof AutoconfForElement)) {
Token peek = tokenizer.peekToken();
if (peek.getType() == ITokenConstants.RPAREN) {
handleError(peek, AutoconfEditorMessages.getString(UNMATCHED_RIGHT_PARENTHESIS));
}
}
return token;
}
private void validateMacroParameterCount(AutoconfMacroElement macro, Token token, int count) {
if (macro.getParameterCount() < count) {
handleError(token, AutoconfEditorMessages.getFormattedString("M4MacroArgsTooFew",
macro.getName(), Integer.valueOf(2))); //$NON-NLS-1$
} else if (macro.getParameterCount() > count) {
handleError(token, AutoconfEditorMessages.getFormattedString("M4MacroArgsTooMany",
macro.getName(), Integer.valueOf(2))); //$NON-NLS-1$
}
}
/**
* Start parsing a macro call at a suspected macro expansion location.
* @param macro
* @param line the line containing the start of the
* @param parent
* @return last token parsed
*/
protected Token parseMacro(AutoconfMacroElement macro, Token macroName) {
setSourceStart(macro, macroName);
// parse any arguments
tokenizer.setM4Context(true);
Token token = tokenizer.readToken();
if (token.getType() == ITokenConstants.LPAREN) {
token = parseMacroArguments(macro);
setSourceEnd(macro, token);
} else {
tokenizer.unreadToken(token);
setSourceEnd(macro, macroName);
}
tokenizer.setM4Context(false);
// validate macro arguments?
if (macroValidator != null) {
try {
macroValidator.validateMacroCall(macro);
} catch (ParseException e) {
errorHandler.handleError(e);
} catch (InvalidMacroException e) {
handleError(e);
}
}
return token;
}
/**
* Parse the arguments for the given macro. These are not interpreted as
* shell constructs but just as text with possibly more macro expansions
* inside.
*
* @param macro
* @return final token (')')
*
* @since 2.0
*/
protected Token parseMacroArguments(AutoconfMacroElement macro) {
Token argStart = null;
Token argEnd = null;
Token token;
// When parsing, we want to ignore the whitespace around the arguments.
// So, instead of taking the source range "between" a parenthesis and a comma,
// track the exact tokens forming the start and end of an argument, defaulting
// to the borders of the parentheses and commas if no text is included.
StringBuilder argBuffer = new StringBuilder();
AutoconfMacroArgumentElement arg = new AutoconfMacroArgumentElement();
while (true) {
token = tokenizer.readToken();
if (token.getType() == ITokenConstants.EOL) {
if (argBuffer.length() > 0)
argBuffer.append(token.getText());
continue;
}
if (token.getType() == ITokenConstants.COMMA
|| token.getType() == ITokenConstants.RPAREN
|| token.getType() == ITokenConstants.EOF) {
arg.setName(argBuffer.toString());
argBuffer.setLength(0);
if (argStart != null && argEnd != null) {
setSourceStart(arg, argStart);
setSourceEnd(arg, argEnd);
} else if (argEnd != null) {
setSourceStart(arg, argStart);
setSourceEndBefore(arg, token);
} else {
// empty argument
setSourceStart(arg, token);
setSourceEndBefore(arg, token);
}
macro.addChild(arg);
if (token.getType() != ITokenConstants.COMMA)
break;
argStart = null;
argEnd = null;
arg = new AutoconfMacroArgumentElement();
} else {
if (argStart == null) {
argStart = token;
}
argEnd = token;
if (argBuffer.length() > 0 && token.followsSpace())
argBuffer.append(' ');
argBuffer.append(token.getText());
// handle nested macro calls in arguments
if (token.getType() == ITokenConstants.WORD) {
argEnd = checkMacro(arg, token);
}
}
}
if (token.getType() != ITokenConstants.RPAREN) {
handleError(token, AutoconfEditorMessages.getString(UNMATCHED_LEFT_PARENTHESIS));
}
// note: moved 15-char truncation to AutoconfLabelProvider
AutoconfElement[] children = macro.getChildren();
if (children.length > 0)
macro.setVar(children[0].getVar());
return token;
}
/** @since 2.0 */
protected void handleError(Token token, String message) {
handleMessage(token, message, IMarker.SEVERITY_ERROR);
}
/** @since 2.0 */
protected void handleWarning(Token token, String message) {
handleMessage(token, message, IMarker.SEVERITY_WARNING);
}
/** @since 2.0 */
protected void handleMessage(Token token, String message, int severity) {
if (errorHandler != null) {
int lineNumber = 0;
int startColumn = 0;
int endColumn = 0;
try {
lineNumber = token.getDocument().getLineOfOffset(token.getOffset());
int lineOffs = token.getDocument().getLineOffset(lineNumber);
startColumn = token.getOffset() - lineOffs;
endColumn = startColumn + token.getLength();
} catch (BadLocationException e) {
// Don't care if we blow up trying to issue marker
}
errorHandler.handleError(new ParseException(
message,
token.getOffset(),
token.getOffset() + token.getLength(),
lineNumber,
startColumn, endColumn,
severity));
}
}
/**
* Figure out the error location and create a marker and message for it.
* @param exception
*/
protected void handleError (InvalidMacroException exception) {
AutoconfElement element = exception.getBadElement();
if (errorHandler != null) {
int lineNumber = 0;
int startColumn = 0;
int endColumn = 0;
try {
lineNumber = element.getDocument().getLineOfOffset(element.getStartOffset());
int lineOffs = element.getDocument().getLineOffset(lineNumber);
startColumn = element.getStartOffset() - lineOffs;
endColumn = startColumn + element.getLength();
} catch (BadLocationException e) {
// Don't care if we blow up trying to issue marker
}
errorHandler.handleError(new ParseException(
exception.getMessage(),
element.getStartOffset(),
element.getEndOffset(),
lineNumber,
startColumn, endColumn,
IMarker.SEVERITY_ERROR));
}
}
public IAutoconfErrorHandler getErrorHandler() {
return errorHandler;
}
}