| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.netbeans.modules.css.lib; |
| |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import org.antlr.runtime.CommonToken; |
| import org.antlr.runtime.RecognitionException; |
| import org.antlr.runtime.Token; |
| import org.antlr.runtime.debug.BlankDebugEventListener; |
| |
| import java.util.Stack; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.LinkedHashSet; |
| import org.antlr.runtime.NoViableAltException; |
| import org.netbeans.modules.css.lib.api.CssTokenId; |
| import org.netbeans.modules.css.lib.api.Node; |
| import org.netbeans.modules.css.lib.api.NodeType; |
| import org.netbeans.modules.css.lib.api.ProblemDescription; |
| import org.openide.util.NbBundle; |
| import static org.netbeans.modules.css.lib.Bundle.*; |
| |
| /** |
| * A patched version of ANLR's ParseTreeBuilder |
| * |
| * @author mfukala@netbeans.org |
| */ |
| public class NbParseTreeBuilder extends BlankDebugEventListener { |
| |
| //ignore 'syncToIdent' rule - the DBG.enter/exit/Rule calls are generated |
| //automatically by ANTLR but we do not care about them since |
| //the error recovery implementation in syncToSet(...) |
| //calls DBG.enter/exit/Rule("recovery") itself. |
| private String[] IGNORED_RULES = new String[]{"syncToDeclarationsRule", "syncToFollow"}; //!!! must be sorted alphabetically !!! |
| Stack<RuleNode> callStack = new Stack<>(); |
| List<CommonToken> hiddenTokens = new ArrayList<>(); |
| private int backtracking = 0; |
| private CommonToken lastConsumedToken; |
| private CharSequence source; |
| static boolean debug_tokens = false; //testing |
| private Stack<ErrorNode> errorNodes = new Stack<>(); |
| private boolean resync; |
| private CommonToken unexpectedToken; |
| |
| private Map<CommonToken, Pair<Node>> noViableAltNodes = new HashMap<>(); |
| private Collection<RuleNode> leafRuleNodes = new ArrayList<>(); |
| |
| private static final String RECOVERY_RULE_NAME = "recovery"; |
| private final Collection<ProblemDescription> problems = new LinkedHashSet<> (); |
| |
| public NbParseTreeBuilder(CharSequence source) { |
| this.source = source; |
| callStack.push(new RootNode(source)); |
| } |
| |
| public AbstractParseTreeNode getTree() { |
| return callStack.elementAt(0); |
| } |
| |
| /** Backtracking or cyclic DFA, don't want to add nodes to tree */ |
| @Override |
| public void enterDecision(int d, boolean couldBacktrack) { |
| backtracking++; |
| } |
| |
| @Override |
| public void exitDecision(int i) { |
| backtracking--; |
| } |
| |
| private boolean isIgnoredRule(String ruleName) { |
| return Arrays.binarySearch(IGNORED_RULES, ruleName) >= 0; |
| } |
| |
| @Override |
| public void enterRule(String filename, String ruleName) { |
| if (backtracking > 0) { |
| return; |
| } |
| if (isIgnoredRule(ruleName)) { |
| return; |
| } |
| |
| AbstractParseTreeNode parentRuleNode = callStack.peek(); |
| RuleNode ruleNode = new RuleNode(NodeType.valueOf(ruleName), source); |
| addNodeChild(parentRuleNode, ruleNode); |
| callStack.push(ruleNode); |
| } |
| |
| @Override |
| public void exitRule(String filename, String ruleName) { |
| if (backtracking > 0) { |
| return; |
| } |
| if (isIgnoredRule(ruleName)) { |
| return; |
| } |
| |
| RuleNode ruleNode = callStack.pop(); |
| if (ruleNode.getChildCount() > 0) { |
| //set the rule end offset |
| if (lastConsumedToken != null) { |
| ruleNode.setLastToken(lastConsumedToken); |
| } |
| } else { |
| //empty node - we cannot remove it right now since an error node |
| //may be attached to it later. |
| //all the nodes from possiblyEmptyRuleNodes list are checked after |
| //the parsing finishes and removed from the parse tree if still empty |
| leafRuleNodes.add(ruleNode); |
| } |
| |
| if(RECOVERY_RULE_NAME.equals(ruleName)) { |
| |
| if(ruleNode.getChildCount() > 0) { |
| //create a ProblemDescription for the skipped tokens |
| //create a ParsingProblem |
| int trimmedSize = 0; |
| StringBuilder tokensList = new StringBuilder(); |
| for(int i = 0; i < ruleNode.getChildCount(); i++) { |
| Node child = (Node)ruleNode.getChild(i); |
| trimmedSize+=child.image().toString().trim().length(); |
| |
| tokensList.append('\''); |
| tokensList.append(child.image()); |
| tokensList.append('\''); |
| if(i < ruleNode.getChildCount() - 1) { |
| tokensList.append(','); |
| } |
| } |
| |
| if(trimmedSize > 0) { |
| //do not report skipped whitespaces |
| ProblemDescription problemDescription = new ProblemDescription( |
| ruleNode.from(), |
| ruleNode.to(), |
| MSG_Error_Unexpected_Char(tokensList), |
| ProblemDescription.Keys.PARSING.name(), |
| ProblemDescription.Type.ERROR); |
| |
| problems.add(problemDescription); |
| } |
| } |
| |
| } |
| } |
| |
| @Override |
| public void beginResync() { |
| super.beginResync(); |
| resync = true; |
| } |
| |
| @Override |
| public void endResync() { |
| super.endResync(); |
| resync = false; |
| } |
| |
| @Override |
| public void consumeToken(Token token) { |
| if (backtracking > 0 || resync) { |
| return; |
| } |
| |
| if (debug_tokens) { |
| CommonToken ct = (CommonToken) token; |
| int[] ctr = CommonTokenUtil.getCommonTokenOffsetRange(ct); |
| System.out.println(token + "(" + ctr[0] + "-" + ctr[1] + ")"); |
| } |
| |
| //ignore the closing EOF token, we do not want it |
| //it the parse tree |
| if (token.getType() == Css3Lexer.EOF) { |
| return; |
| } |
| |
| //also ignore error tokens - they are added as children of ErrorNode-s in the recognitionException(...) method |
| if (token.getType() == Token.INVALID_TOKEN_TYPE) { |
| return; |
| } |
| |
| lastConsumedToken = (CommonToken) token; |
| |
| RuleNode ruleNode = callStack.peek(); |
| TokenNode elementNode = new TokenNode(source, (CommonToken) token); |
| elementNode.hiddenTokens = this.hiddenTokens; |
| hiddenTokens.clear(); |
| ruleNode.addChild(elementNode); |
| |
| updateFirstTokens(ruleNode, lastConsumedToken); |
| } |
| |
| //set first token for all RuleNode-s in the stack without the first token set |
| private void updateFirstTokens(RuleNode ruleNode, CommonToken token) { |
| while (true) { |
| |
| if (ruleNode.from() != -1) { |
| break; |
| } |
| ruleNode.setFirstToken(token); |
| ruleNode = (RuleNode) ruleNode.getParent(); |
| if (ruleNode == null) { |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void consumeHiddenToken(Token token) { |
| if (backtracking > 0 || resync) { |
| return; |
| } |
| |
| if (debug_tokens) { |
| CommonToken ct = (CommonToken) token; |
| int[] ctr = CommonTokenUtil.getCommonTokenOffsetRange(ct); |
| System.out.println(token + "(" + ctr[0] + "-" + ctr[1] + ")"); |
| } |
| |
| hiddenTokens.add((CommonToken) token); |
| } |
| |
| @Override |
| @NbBundle.Messages({ |
| "# {0} - the unexpected token", |
| "MSG_Error_Unexpected_Token=Unexpected token {0} found", |
| "MSG_Error_Premature_EOF=Premature end of file"}) |
| public void recognitionException(RecognitionException e) { |
| if (backtracking > 0) { |
| return; |
| } |
| |
| RuleNode ruleNode = callStack.peek(); |
| |
| String message; |
| int from, to; |
| |
| assert e.token != null; |
| |
| //invalid token found int the stream |
| unexpectedToken = (CommonToken) e.token; |
| int unexpectedTokenCode = e.getUnexpectedType(); |
| CssTokenId unexpectedTokenId = CssTokenId.forTokenTypeCode(unexpectedTokenCode); |
| |
| assert unexpectedTokenId != null : "No CssTokenId for " + unexpectedToken; |
| |
| //special handling for EOF token - it has lenght == 1 ! |
| if(unexpectedTokenId == CssTokenId.EOF) { |
| from = to = CommonTokenUtil.getCommonTokenOffsetRange(unexpectedToken)[0]; |
| } else { |
| //normal tokens |
| from = CommonTokenUtil.getCommonTokenOffsetRange(unexpectedToken)[0]; |
| to = CommonTokenUtil.getCommonTokenOffsetRange(unexpectedToken)[1]; |
| } |
| |
| if (unexpectedTokenId == CssTokenId.EOF) { |
| message = MSG_Error_Premature_EOF(); |
| } else { |
| message = MSG_Error_Unexpected_Token(unexpectedTokenId.name()); |
| } |
| |
| //create a ParsingProblem |
| ProblemDescription problemDescription = new ProblemDescription( |
| from, |
| to, |
| message, |
| ProblemDescription.Keys.PARSING.name(), |
| ProblemDescription.Type.ERROR); |
| |
| problems.add(problemDescription); |
| |
| //create an error node and add it to the parse tree |
| ErrorNode errorNode = new ErrorNode(from, to, problemDescription, source); |
| |
| //add the unexpected token as a child of the error node |
| TokenNode tokenNode = new TokenNode(source, unexpectedToken); |
| addNodeChild(errorNode, tokenNode); |
| |
| if(e instanceof NoViableAltException) { |
| //error during predicate - the unexpected token may or may not be |
| //reported later as an error. To handle this, |
| //store the error node and the ruleNode where the error node should be added |
| noViableAltNodes.put(unexpectedToken, new Pair<Node>(ruleNode, errorNode)); |
| errorNodes.push(errorNode); |
| } else { |
| //possibly remove the unexpectedToken from the noViableAltNodes map |
| |
| //NOTICE: |
| //Uncomment the following line if you want the parse tree not to produce |
| //multiple error nodes for the same token. If the line is active, there |
| //wont be error nodes for semantic predicates if the unexpected token |
| //is matched by another error rule later. |
| // noViableAltNodes.remove(unexpectedToken); |
| |
| addNodeChild(ruleNode, errorNode); |
| errorNodes.push(errorNode); |
| |
| //create and artificial error token so the rules on stack can properly set their ranges |
| lastConsumedToken = new CommonToken(Token.INVALID_TOKEN_TYPE); |
| lastConsumedToken.setStartIndex(from); |
| lastConsumedToken.setStopIndex(to - 1); // ... ( -1 => last *char* index ) |
| } |
| |
| |
| } |
| |
| @Override |
| public void terminate() { |
| super.terminate(); |
| |
| //process unreported errors from NoViableAltException |
| for(Pair<Node> pair : noViableAltNodes.values()) { |
| RuleNode ruleNode = (RuleNode)pair.n1; |
| ErrorNode errorNode = (ErrorNode)pair.n2; |
| |
| ruleNode.addChild(errorNode); |
| errorNode.setParent(ruleNode); |
| } |
| |
| //Finally after the parsing is done fix the error nodes and their predecessors. |
| //This fixes the problem with rules where RecognitionException happened |
| //but the errorneous or missing token has been matched in somewhere further |
| for (ErrorNode en : errorNodes) { |
| synchronizeAncestorsBoundaries(en); |
| } |
| |
| //clean parse tree from empty rule nodes |
| //empty rule node == rule node without a single _token node_ child |
| for(RuleNode node : leafRuleNodes) { |
| removeLeafRuleNodes(node); |
| } |
| |
| } |
| |
| //removes all empty rule nodes in the tree path from the given node to the parse tree root |
| private void removeLeafRuleNodes(RuleNode node) { |
| for(;;) { |
| if(node.children().isEmpty()) { |
| RuleNode parent = (RuleNode)node.parent(); |
| if(parent == null) { |
| return ; |
| } |
| parent.deleteChild(node); |
| node = parent; |
| } else { |
| break; |
| } |
| } |
| } |
| |
| private void synchronizeAncestorsBoundaries(RuleNode en) { |
| RuleNode n = en; |
| for (;;) { |
| if (n == null) { |
| break; |
| } |
| |
| //adjust the parent nodes ranges to the errorNode |
| if (n.from() == -1 || n.from() > en.from()) { |
| n.from = en.from(); |
| } |
| if(n.to() == -1 || n.to() < en.to()) { |
| n.to = en.to(); |
| } |
| |
| n = (RuleNode) n.parent(); |
| } |
| } |
| |
| public Collection<ProblemDescription> getProblems() { |
| return problems; |
| } |
| |
| //note: it would be possible to handle this all in consumeToken since it is called from the |
| //BaseRecognizer.consumeUntil(...) { input.consume(); } but for the better usability |
| //it is done this way. So the beginResyn/endResync doesn't have to be used. |
| //the NbParseTreeBuilder.consumeToken() method ignores tokens with ERROR type so they |
| //won't be duplicated in the parse tree |
| |
| //creates a "recovery" node with all the skipped tokens as children |
| void consumeSkippedTokens(List<Token> tokens) { |
| if(tokens.isEmpty()) { |
| return ; |
| } |
| |
| CommonToken first = (CommonToken)tokens.get(0); |
| CommonToken last = (CommonToken)tokens.get(tokens.size() - 1); |
| |
| |
| |
| //if there's just one recovered token and the token is the same as the unexpectedToken just skip the |
| //recovery node creation, the parse tree for the errorneous piece of code is already complete |
| boolean ignoreFirstToken = unexpectedToken == first; |
| if(ignoreFirstToken && tokens.size() == 1) { |
| return ; |
| } |
| |
| //do not add the first token as children of the recovery node if it has been already |
| //added as a child of the error node created for the RecognitionException |
| if(ignoreFirstToken) { |
| first = (CommonToken)tokens.get(1); //use second |
| } |
| |
| //find last error which triggered this recovery and add the skipped tokens to it |
| // ErrorNode errorNode = errorNodes.peek(); |
| // RuleNode peek = callStack.peek(); |
| // if(!(peek instanceof ErrorNode)) { |
| |
| RuleNode peek = errorNodes.peek(); |
| |
| RuleNode node = new RuleNode(NodeType.recovery, source); |
| peek.addChild(node); |
| node.setParent(peek); |
| peek = node; |
| |
| // } |
| |
| |
| //set first and last token |
| peek.setFirstToken(first); |
| peek.setLastToken(last); |
| |
| synchronizeAncestorsBoundaries(peek); |
| |
| //set range |
| peek.from = CommonTokenUtil.getCommonTokenOffsetRange(first)[0]; |
| peek.to = CommonTokenUtil.getCommonTokenOffsetRange(last)[1]; |
| |
| //set the error tokens as children of the error node |
| for(int i = (ignoreFirstToken ? 1 : 0); i < tokens.size(); i++) { |
| CommonToken token = (CommonToken)tokens.get(i); |
| TokenNode tokenNode = new TokenNode(source, token); |
| addNodeChild(peek, tokenNode); |
| } |
| |
| //create and artificial error token so the rules on stack can properly set their ranges |
| lastConsumedToken = new CommonToken(Token.INVALID_TOKEN_TYPE); |
| lastConsumedToken.setStartIndex(first.getStartIndex()); |
| lastConsumedToken.setStopIndex(last.getStopIndex()); |
| |
| } |
| |
| |
| private void addNodeChild(AbstractParseTreeNode parent, AbstractParseTreeNode child) { |
| parent.addChild(child); |
| child.setParent(parent); |
| } |
| |
| private static class Pair<T> { |
| T n1, n2; |
| public Pair(T n1, T n2) { |
| this.n1 = n1; |
| this.n2 = n2; |
| } |
| } |
| |
| } |