/*
* Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
* Use of this file is governed by the BSD 3-clause license that
* can be found in the LICENSE.txt file in the project root.
*/
package org.antlr.v4.semantics;
import org.antlr.runtime.Token;
import org.antlr.runtime.tree.CommonTree;
import org.antlr.runtime.tree.Tree;
import org.antlr.v4.misc.Utils;
import org.antlr.v4.parse.ANTLRParser;
import org.antlr.v4.parse.GrammarTreeVisitor;
import org.antlr.v4.tool.ErrorManager;
import org.antlr.v4.tool.ErrorType;
import org.antlr.v4.tool.Grammar;
import org.antlr.v4.tool.Rule;
import org.antlr.v4.tool.ast.ActionAST;
import org.antlr.v4.tool.ast.AltAST;
import org.antlr.v4.tool.ast.BlockAST;
import org.antlr.v4.tool.ast.GrammarAST;
import org.antlr.v4.tool.ast.GrammarASTWithOptions;
import org.antlr.v4.tool.ast.GrammarRootAST;
import org.antlr.v4.tool.ast.RuleAST;
import org.antlr.v4.tool.ast.RuleRefAST;
import org.antlr.v4.tool.ast.TerminalAST;
import org.stringtemplate.v4.misc.MultiMap;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
No side-effects except for setting options into the appropriate node.
TODO: make the side effects into a separate pass this
Invokes check rules for these:
FILE_AND_GRAMMAR_NAME_DIFFER
LEXER_RULES_NOT_ALLOWED
PARSER_RULES_NOT_ALLOWED
CANNOT_ALIAS_TOKENS
ARGS_ON_TOKEN_REF
ILLEGAL_OPTION
REWRITE_OR_OP_WITH_NO_OUTPUT_OPTION
NO_RULES
REWRITE_FOR_MULTI_ELEMENT_ALT
HETERO_ILLEGAL_IN_REWRITE_ALT
AST_OP_WITH_NON_AST_OUTPUT_OPTION
AST_OP_IN_ALT_WITH_REWRITE
CONFLICTING_OPTION_IN_TREE_FILTER
WILDCARD_AS_ROOT
INVALID_IMPORT
TOKEN_VOCAB_IN_DELEGATE
IMPORT_NAME_CLASH
REPEATED_PREQUEL
TOKEN_NAMES_MUST_START_UPPER
/** No side-effects except for setting options into the appropriate node.
* TODO: make the side effects into a separate pass this
*
* Invokes check rules for these:
*
* FILE_AND_GRAMMAR_NAME_DIFFER
* LEXER_RULES_NOT_ALLOWED
* PARSER_RULES_NOT_ALLOWED
* CANNOT_ALIAS_TOKENS
* ARGS_ON_TOKEN_REF
* ILLEGAL_OPTION
* REWRITE_OR_OP_WITH_NO_OUTPUT_OPTION
* NO_RULES
* REWRITE_FOR_MULTI_ELEMENT_ALT
* HETERO_ILLEGAL_IN_REWRITE_ALT
* AST_OP_WITH_NON_AST_OUTPUT_OPTION
* AST_OP_IN_ALT_WITH_REWRITE
* CONFLICTING_OPTION_IN_TREE_FILTER
* WILDCARD_AS_ROOT
* INVALID_IMPORT
* TOKEN_VOCAB_IN_DELEGATE
* IMPORT_NAME_CLASH
* REPEATED_PREQUEL
* TOKEN_NAMES_MUST_START_UPPER
*/
public class BasicSemanticChecks extends GrammarTreeVisitor {
Set of valid imports. Maps delegate to set of delegator grammar types.
validDelegations.get(LEXER) gives list of the kinds of delegators
that can import lexers.
/** Set of valid imports. Maps delegate to set of delegator grammar types.
* validDelegations.get(LEXER) gives list of the kinds of delegators
* that can import lexers.
*/
public static MultiMap<Integer,Integer> validImportTypes =
new MultiMap<Integer,Integer>() {
{
map(ANTLRParser.LEXER, ANTLRParser.LEXER);
map(ANTLRParser.LEXER, ANTLRParser.COMBINED);
map(ANTLRParser.PARSER, ANTLRParser.PARSER);
map(ANTLRParser.PARSER, ANTLRParser.COMBINED);
map(ANTLRParser.COMBINED, ANTLRParser.COMBINED);
}
};
public Grammar g;
public RuleCollector ruleCollector;
public ErrorManager errMgr;
When this is true
, the semantic checks will report ErrorType.UNRECOGNIZED_ASSOC_OPTION
where appropriate. This may be set to false
to disable this specific check. The default value is true
.
/**
* When this is {@code true}, the semantic checks will report
* {@link ErrorType#UNRECOGNIZED_ASSOC_OPTION} where appropriate. This may
* be set to {@code false} to disable this specific check.
*
* <p>The default value is {@code true}.</p>
*/
public boolean checkAssocElementOption = true;
This field is used for reporting the ErrorType.MODE_WITHOUT_RULES
error when necessary. /**
* This field is used for reporting the {@link ErrorType#MODE_WITHOUT_RULES}
* error when necessary.
*/
protected int nonFragmentRuleCount;
This is true
from the time discoverLexerRule
is called for a lexer rule with the fragment
modifier until exitLexerRule
is called. /**
* This is {@code true} from the time {@link #discoverLexerRule} is called
* for a lexer rule with the {@code fragment} modifier until
* {@link #exitLexerRule} is called.
*/
private boolean inFragmentRule;
public BasicSemanticChecks(Grammar g, RuleCollector ruleCollector) {
this.g = g;
this.ruleCollector = ruleCollector;
this.errMgr = g.tool.errMgr;
}
@Override
public ErrorManager getErrorManager() { return errMgr; }
public void process() { visitGrammar(g.ast); }
// Routines to route visitor traffic to the checking routines
@Override
public void discoverGrammar(GrammarRootAST root, GrammarAST ID) {
checkGrammarName(ID.token);
}
@Override
public void finishPrequels(GrammarAST firstPrequel) {
if ( firstPrequel==null ) return;
GrammarAST parent = (GrammarAST)firstPrequel.parent;
List<GrammarAST> options = parent.getAllChildrenWithType(OPTIONS);
List<GrammarAST> imports = parent.getAllChildrenWithType(IMPORT);
List<GrammarAST> tokens = parent.getAllChildrenWithType(TOKENS_SPEC);
checkNumPrequels(options, imports, tokens);
}
@Override
public void importGrammar(GrammarAST label, GrammarAST ID) {
checkImport(ID.token);
}
@Override
public void discoverRules(GrammarAST rules) {
checkNumRules(rules);
}
@Override
protected void enterMode(GrammarAST tree) {
nonFragmentRuleCount = 0;
}
@Override
protected void exitMode(GrammarAST tree) {
if (nonFragmentRuleCount == 0) {
Token token = tree.getToken();
String name = "?";
if (tree.getChildCount() > 0) {
name = tree.getChild(0).getText();
if (name == null || name.isEmpty()) {
name = "?";
}
token = ((GrammarAST)tree.getChild(0)).getToken();
}
g.tool.errMgr.grammarError(ErrorType.MODE_WITHOUT_RULES, g.fileName, token, name, g);
}
}
@Override
public void modeDef(GrammarAST m, GrammarAST ID) {
if ( !g.isLexer() ) {
g.tool.errMgr.grammarError(ErrorType.MODE_NOT_IN_LEXER, g.fileName,
ID.token, ID.token.getText(), g);
}
}
@Override
public void discoverRule(RuleAST rule, GrammarAST ID,
List<GrammarAST> modifiers,
ActionAST arg, ActionAST returns,
GrammarAST thrws, GrammarAST options,
ActionAST locals,
List<GrammarAST> actions, GrammarAST block)
{
// TODO: chk that all or no alts have "# label"
checkInvalidRuleDef(ID.token);
}
@Override
public void discoverLexerRule(RuleAST rule, GrammarAST ID, List<GrammarAST> modifiers,
GrammarAST block)
{
checkInvalidRuleDef(ID.token);
if (modifiers != null) {
for (GrammarAST tree : modifiers) {
if (tree.getType() == ANTLRParser.FRAGMENT) {
inFragmentRule = true;
}
}
}
if (!inFragmentRule) {
nonFragmentRuleCount++;
}
}
@Override
protected void exitLexerRule(GrammarAST tree) {
inFragmentRule = false;
}
@Override
public void ruleRef(GrammarAST ref, ActionAST arg) {
checkInvalidRuleRef(ref.token);
}
@Override
public void ruleOption(GrammarAST ID, GrammarAST valueAST) {
checkOptions((GrammarAST)ID.getAncestor(RULE), ID.token, valueAST);
}
@Override
public void blockOption(GrammarAST ID, GrammarAST valueAST) {
checkOptions((GrammarAST)ID.getAncestor(BLOCK), ID.token, valueAST);
}
@Override
public void grammarOption(GrammarAST ID, GrammarAST valueAST) {
boolean ok = checkOptions(g.ast, ID.token, valueAST);
//if ( ok ) g.ast.setOption(ID.getText(), value);
}
@Override
public void defineToken(GrammarAST ID) {
checkTokenDefinition(ID.token);
}
@Override
protected void enterChannelsSpec(GrammarAST tree) {
if (g.isParser()) {
g.tool.errMgr.grammarError(ErrorType.CHANNELS_BLOCK_IN_PARSER_GRAMMAR, g.fileName, tree.token);
}
else if (g.isCombined()) {
g.tool.errMgr.grammarError(ErrorType.CHANNELS_BLOCK_IN_COMBINED_GRAMMAR, g.fileName, tree.token);
}
}
@Override
public void defineChannel(GrammarAST ID) {
checkChannelDefinition(ID.token);
}
@Override
public void elementOption(GrammarASTWithOptions elem, GrammarAST ID, GrammarAST valueAST) {
String v = null;
boolean ok = checkElementOptions(elem, ID, valueAST);
// if ( ok ) {
// if ( v!=null ) {
// t.setOption(ID.getText(), v);
// }
// else {
// t.setOption(TerminalAST.defaultTokenOption, v);
// }
// }
}
@Override
public void finishRule(RuleAST rule, GrammarAST ID, GrammarAST block) {
if ( rule.isLexerRule() ) return;
BlockAST blk = (BlockAST)rule.getFirstChildWithType(BLOCK);
int nalts = blk.getChildCount();
GrammarAST idAST = (GrammarAST)rule.getChild(0);
for (int i=0; i< nalts; i++) {
AltAST altAST = (AltAST)blk.getChild(i);
if ( altAST.altLabel!=null ) {
String altLabel = altAST.altLabel.getText();
// first check that label doesn't conflict with a rule
// label X or x can't be rule x.
Rule r = ruleCollector.rules.get(Utils.decapitalize(altLabel));
if ( r!=null ) {
g.tool.errMgr.grammarError(ErrorType.ALT_LABEL_CONFLICTS_WITH_RULE,
g.fileName, altAST.altLabel.token,
altLabel,
r.name);
}
// Now verify that label X or x doesn't conflict with label
// in another rule. altLabelToRuleName has both X and x mapped.
String prevRuleForLabel = ruleCollector.altLabelToRuleName.get(altLabel);
if ( prevRuleForLabel!=null && !prevRuleForLabel.equals(rule.getRuleName()) ) {
g.tool.errMgr.grammarError(ErrorType.ALT_LABEL_REDEF,
g.fileName, altAST.altLabel.token,
altLabel,
rule.getRuleName(),
prevRuleForLabel);
}
}
}
List<GrammarAST> altLabels = ruleCollector.ruleToAltLabels.get(rule.getRuleName());
int numAltLabels = 0;
if ( altLabels!=null ) numAltLabels = altLabels.size();
if ( numAltLabels>0 && nalts != numAltLabels ) {
g.tool.errMgr.grammarError(ErrorType.RULE_WITH_TOO_FEW_ALT_LABELS,
g.fileName, idAST.token, rule.getRuleName());
}
}
// Routines to do the actual work of checking issues with a grammar.
// They are triggered by the visitor methods above.
void checkGrammarName(Token nameToken) {
String fullyQualifiedName = nameToken.getInputStream().getSourceName();
if (fullyQualifiedName == null) {
// This wasn't read from a file.
return;
}
File f = new File(fullyQualifiedName);
String fileName = f.getName();
if ( g.originalGrammar!=null ) return; // don't warn about diff if this is implicit lexer
if ( !Utils.stripFileExtension(fileName).equals(nameToken.getText()) &&
!fileName.equals(Grammar.GRAMMAR_FROM_STRING_NAME)) {
g.tool.errMgr.grammarError(ErrorType.FILE_AND_GRAMMAR_NAME_DIFFER,
fileName, nameToken, nameToken.getText(), fileName);
}
}
void checkNumRules(GrammarAST rulesNode) {
if ( rulesNode.getChildCount()==0 ) {
GrammarAST root = (GrammarAST)rulesNode.getParent();
GrammarAST IDNode = (GrammarAST)root.getChild(0);
g.tool.errMgr.grammarError(ErrorType.NO_RULES, g.fileName,
null, IDNode.getText(), g);
}
}
void checkNumPrequels(List<GrammarAST> options,
List<GrammarAST> imports,
List<GrammarAST> tokens)
{
List<Token> secondOptionTokens = new ArrayList<Token>();
if ( options!=null && options.size()>1 ) {
secondOptionTokens.add(options.get(1).token);
}
if ( imports!=null && imports.size()>1 ) {
secondOptionTokens.add(imports.get(1).token);
}
if ( tokens!=null && tokens.size()>1 ) {
secondOptionTokens.add(tokens.get(1).token);
}
for (Token t : secondOptionTokens) {
String fileName = t.getInputStream().getSourceName();
g.tool.errMgr.grammarError(ErrorType.REPEATED_PREQUEL,
fileName, t);
}
}
void checkInvalidRuleDef(Token ruleID) {
String fileName = null;
if ( ruleID.getInputStream()!=null ) {
fileName = ruleID.getInputStream().getSourceName();
}
if ( g.isLexer() && Character.isLowerCase(ruleID.getText().charAt(0)) ) {
g.tool.errMgr.grammarError(ErrorType.PARSER_RULES_NOT_ALLOWED,
fileName, ruleID, ruleID.getText());
}
if ( g.isParser() &&
Grammar.isTokenName(ruleID.getText()) )
{
g.tool.errMgr.grammarError(ErrorType.LEXER_RULES_NOT_ALLOWED,
fileName, ruleID, ruleID.getText());
}
}
void checkInvalidRuleRef(Token ruleID) {
String fileName = ruleID.getInputStream().getSourceName();
if ( g.isLexer() && Character.isLowerCase(ruleID.getText().charAt(0)) ) {
g.tool.errMgr.grammarError(ErrorType.PARSER_RULE_REF_IN_LEXER_RULE,
fileName, ruleID, ruleID.getText(), currentRuleName);
}
}
void checkTokenDefinition(Token tokenID) {
String fileName = tokenID.getInputStream().getSourceName();
if ( !Grammar.isTokenName(tokenID.getText()) ) {
g.tool.errMgr.grammarError(ErrorType.TOKEN_NAMES_MUST_START_UPPER,
fileName,
tokenID,
tokenID.getText());
}
}
void checkChannelDefinition(Token tokenID) {
}
@Override
protected void enterLexerElement(GrammarAST tree) {
}
@Override
protected void enterLexerCommand(GrammarAST tree) {
checkElementIsOuterMostInSingleAlt(tree);
if (inFragmentRule) {
String fileName = tree.token.getInputStream().getSourceName();
String ruleName = currentRuleName;
g.tool.errMgr.grammarError(ErrorType.FRAGMENT_ACTION_IGNORED, fileName, tree.token, ruleName);
}
}
@Override
public void actionInAlt(ActionAST action) {
if (inFragmentRule) {
String fileName = action.token.getInputStream().getSourceName();
String ruleName = currentRuleName;
g.tool.errMgr.grammarError(ErrorType.FRAGMENT_ACTION_IGNORED, fileName, action.token, ruleName);
}
}
Make sure that action is last element in outer alt; here action,
a2, z, and zz are bad, but a3 is ok:
(RULE A (BLOCK (ALT {action} 'a')))
(RULE B (BLOCK (ALT (BLOCK (ALT {a2} 'x') (ALT 'y')) {a3})))
(RULE C (BLOCK (ALT 'd' {z}) (ALT 'e' {zz})))
/**
Make sure that action is last element in outer alt; here action,
a2, z, and zz are bad, but a3 is ok:
(RULE A (BLOCK (ALT {action} 'a')))
(RULE B (BLOCK (ALT (BLOCK (ALT {a2} 'x') (ALT 'y')) {a3})))
(RULE C (BLOCK (ALT 'd' {z}) (ALT 'e' {zz})))
*/
protected void checkElementIsOuterMostInSingleAlt(GrammarAST tree) {
CommonTree alt = tree.parent;
CommonTree blk = alt.parent;
boolean outerMostAlt = blk.parent.getType() == RULE;
Tree rule = tree.getAncestor(RULE);
String fileName = tree.getToken().getInputStream().getSourceName();
if ( !outerMostAlt || blk.getChildCount()>1 )
{
ErrorType e = ErrorType.LEXER_COMMAND_PLACEMENT_ISSUE;
g.tool.errMgr.grammarError(e,
fileName,
tree.getToken(),
rule.getChild(0).getText());
}
}
@Override
public void label(GrammarAST op, GrammarAST ID, GrammarAST element) {
switch (element.getType()) {
// token atoms
case TOKEN_REF:
case STRING_LITERAL:
case RANGE:
// token sets
case SET:
case NOT:
// rule atoms
case RULE_REF:
case WILDCARD:
return;
default:
String fileName = ID.token.getInputStream().getSourceName();
g.tool.errMgr.grammarError(ErrorType.LABEL_BLOCK_NOT_A_SET, fileName, ID.token, ID.getText());
break;
}
}
@Override
protected void enterLabeledLexerElement(GrammarAST tree) {
Token label = ((GrammarAST)tree.getChild(0)).getToken();
g.tool.errMgr.grammarError(ErrorType.V3_LEXER_LABEL,
g.fileName,
label,
label.getText());
}
@Override
protected void enterTerminal(GrammarAST tree) {
String text = tree.getText();
if (text.equals("''")) {
g.tool.errMgr.grammarError(ErrorType.EMPTY_STRINGS_AND_SETS_NOT_ALLOWED, g.fileName, tree.token, "''");
}
}
Check option is appropriate for grammar, rule, subrule /** Check option is appropriate for grammar, rule, subrule */
boolean checkOptions(GrammarAST parent,
Token optionID,
GrammarAST valueAST)
{
boolean ok = true;
if ( parent.getType()==ANTLRParser.BLOCK ) {
if ( g.isLexer() && !Grammar.LexerBlockOptions.contains(optionID.getText()) ) { // block
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
g.fileName,
optionID,
optionID.getText());
ok = false;
}
if ( !g.isLexer() && !Grammar.ParserBlockOptions.contains(optionID.getText()) ) { // block
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
g.fileName,
optionID,
optionID.getText());
ok = false;
}
}
else if ( parent.getType()==ANTLRParser.RULE ) {
if ( !Grammar.ruleOptions.contains(optionID.getText()) ) { // rule
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
g.fileName,
optionID,
optionID.getText());
ok = false;
}
}
else if ( parent.getType()==ANTLRParser.GRAMMAR &&
!legalGrammarOption(optionID.getText()) ) { // grammar
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
g.fileName,
optionID,
optionID.getText());
ok = false;
}
return ok;
}
Check option is appropriate for elem; parent of ID is ELEMENT_OPTIONS /** Check option is appropriate for elem; parent of ID is ELEMENT_OPTIONS */
boolean checkElementOptions(GrammarASTWithOptions elem,
GrammarAST ID,
GrammarAST valueAST)
{
if (checkAssocElementOption && ID != null && "assoc".equals(ID.getText())) {
if (elem.getType() != ANTLRParser.ALT) {
Token optionID = ID.token;
String fileName = optionID.getInputStream().getSourceName();
g.tool.errMgr.grammarError(ErrorType.UNRECOGNIZED_ASSOC_OPTION,
fileName,
optionID,
currentRuleName);
}
}
if ( elem instanceof RuleRefAST ) {
return checkRuleRefOptions((RuleRefAST)elem, ID, valueAST);
}
if ( elem instanceof TerminalAST ) {
return checkTokenOptions((TerminalAST)elem, ID, valueAST);
}
if ( elem.getType()==ANTLRParser.ACTION ) {
return false;
}
if ( elem.getType()==ANTLRParser.SEMPRED ) {
Token optionID = ID.token;
String fileName = optionID.getInputStream().getSourceName();
if ( valueAST!=null && !Grammar.semPredOptions.contains(optionID.getText()) ) {
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
fileName,
optionID,
optionID.getText());
return false;
}
}
return false;
}
boolean checkRuleRefOptions(RuleRefAST elem, GrammarAST ID, GrammarAST valueAST) {
Token optionID = ID.token;
String fileName = optionID.getInputStream().getSourceName();
// don't care about id<SimpleValue> options
if ( valueAST!=null && !Grammar.ruleRefOptions.contains(optionID.getText()) ) {
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
fileName,
optionID,
optionID.getText());
return false;
}
// TODO: extra checks depending on rule kind?
return true;
}
boolean checkTokenOptions(TerminalAST elem, GrammarAST ID, GrammarAST valueAST) {
Token optionID = ID.token;
String fileName = optionID.getInputStream().getSourceName();
// don't care about ID<ASTNodeName> options
if ( valueAST!=null && !Grammar.tokenOptions.contains(optionID.getText()) ) {
g.tool.errMgr.grammarError(ErrorType.ILLEGAL_OPTION,
fileName,
optionID,
optionID.getText());
return false;
}
// TODO: extra checks depending on terminal kind?
return true;
}
boolean legalGrammarOption(String key) {
switch ( g.getType() ) {
case ANTLRParser.LEXER :
return Grammar.lexerOptions.contains(key);
case ANTLRParser.PARSER :
return Grammar.parserOptions.contains(key);
default :
return Grammar.parserOptions.contains(key);
}
}
void checkImport(Token importID) {
Grammar delegate = g.getImportedGrammar(importID.getText());
if ( delegate==null ) return;
List<Integer> validDelegators = validImportTypes.get(delegate.getType());
if ( validDelegators!=null && !validDelegators.contains(g.getType()) ) {
g.tool.errMgr.grammarError(ErrorType.INVALID_IMPORT,
g.fileName,
importID,
g, delegate);
}
if ( g.isCombined() &&
(delegate.name.equals(g.name+Grammar.getGrammarTypeToFileNameSuffix(ANTLRParser.LEXER))||
delegate.name.equals(g.name+Grammar.getGrammarTypeToFileNameSuffix(ANTLRParser.PARSER))) )
{
g.tool.errMgr.grammarError(ErrorType.IMPORT_NAME_CLASH,
g.fileName,
importID,
g, delegate);
}
}
}