1 /* 2 * Copyright (c) 2012-2019 The ANTLR Project. All rights reserved. 3 * Use of this file is governed by the BSD 3-clause license that 4 * can be found in the LICENSE.txt file in the project root. 5 */ 6 7 module antlr.v4.runtime.tree.pattern.ParseTreePatternMatcher; 8 9 import std.uni; 10 import std.conv; 11 import std.string; 12 import antlr.v4.runtime.Parser; 13 import antlr.v4.runtime.ANTLRInputStream; 14 import antlr.v4.runtime.BailErrorStrategy; 15 import antlr.v4.runtime.Lexer; 16 import antlr.v4.runtime.IllegalArgumentException; 17 import antlr.v4.runtime.ListTokenSource; 18 import antlr.v4.runtime.ParserInterpreter; 19 import antlr.v4.runtime.ParserRuleContext; 20 import antlr.v4.runtime.RuleContext; 21 import antlr.v4.runtime.RecognitionException; 22 import antlr.v4.runtime.Token; 23 import antlr.v4.runtime.TokenConstantDefinition; 24 import antlr.v4.runtime.CommonTokenStream; 25 import antlr.v4.runtime.atn.ATNSimulator; 26 import antlr.v4.runtime.tree.ParseTree; 27 import antlr.v4.runtime.tree.CannotInvokeStartRule; 28 import antlr.v4.runtime.tree.StartRuleDoesNotConsumeFullPattern; 29 import antlr.v4.runtime.tree.TerminalNode; 30 import antlr.v4.runtime.tree.RuleNode; 31 import antlr.v4.runtime.tree.pattern.Chunk; 32 import antlr.v4.runtime.tree.pattern.TagChunk; 33 import antlr.v4.runtime.tree.pattern.TextChunk; 34 import antlr.v4.runtime.tree.pattern.TokenTagToken; 35 import antlr.v4.runtime.tree.pattern.ParseTreePattern; 36 import antlr.v4.runtime.tree.pattern.ParseTreeMatch; 37 import antlr.v4.runtime.tree.pattern.RuleTagToken; 38 import antlr.v4.runtime.misc.ParseCancellationException; 39 40 /** 41 * A tree pattern matching mechanism for ANTLR {@link ParseTree}s. 42 * 43 * Patterns are strings of source input text with special tags representing 44 * token or rule references such as: 45 * 46 * <p>{@code <ID> = <expr>;}</p> 47 * 48 * Given a pattern start rule such as {@code statement}, this object constructs 49 * a {@link ParseTree} with placeholders for the {@code ID} and {@code expr} 50 * subtree. Then the {@link #match} routines can compare an actual 51 * {@link ParseTree} from a parse with this pattern. Tag {@code <ID>} matches 52 * any {@code ID} token and tag {@code <expr>} references the result of the 53 * {@code expr} rule (generally an instance of {@code ExprContext}). 54 * 55 * Pattern {@code x = 0;} is a similar pattern that matches the same pattern 56 * except that it requires the identifier to be {@code x} and the expression to 57 * be {@code 0}. 58 * 59 * The {@link #matches} routines return {@code true} or {@code false} based 60 * upon a match for the tree rooted at the parameter sent in. The 61 * {@link #match} routines return a {@link ParseTreeMatch} object that 62 * contains the parse tree, the parse tree pattern, and a map from tag name to 63 * matched nodes (more below). A subtree that fails to match, returns with 64 * {@link ParseTreeMatch#mismatchedNode} set to the first tree node that did not 65 * match. 66 * 67 * For efficiency, you can compile a tree pattern in string form to a 68 * {@link ParseTreePattern} object. 69 * 70 * See {@code TestParseTreeMatcher} for lots of examples. 71 * {@link ParseTreePattern} has two static helper methods: 72 * {@link ParseTreePattern#findAll} and {@link ParseTreePattern#match} that 73 * are easy to use but not super efficient because they create new 74 * {@link ParseTreePatternMatcher} objects each time and have to compile the 75 * pattern in string form before using it. 76 * 77 * The lexer and parser that you pass into the {@link ParseTreePatternMatcher} 78 * constructor are used to parse the pattern in string form. The lexer converts 79 * the {@code <ID> = <expr>;} into a sequence of four tokens (assuming lexer 80 * throws out whitespace or puts it on a hidden channel). Be aware that the 81 * input stream is reset for the lexer (but not the parser; a 82 * {@link ParserInterpreter} is created to parse the input.). Any user-defined 83 * fields you have put into the lexer might get changed when this mechanism asks 84 * it to scan the pattern string. 85 * 86 * Normally a parser does not accept token {@code <expr>} as a valid 87 * {@code expr} but, from the parser passed in, we create a special version of 88 * the underlying grammar representation (an {@link ATN}) that allows imaginary 89 * tokens representing rules ({@code <expr>}) to match entire rules. We call 90 * these <em>bypass alternatives</em>. 91 * 92 * Delimiters are {@code <} and {@code >}, with {@code \} as the escape string 93 * by default, but you can set them to whatever you want using 94 * {@link #setDelimiters}. You must escape both start and stop strings 95 * {@code \<} and {@code \>}. 96 */ 97 class ParseTreePatternMatcher 98 { 99 100 /** 101 * This is the backing field for {@link #getLexer()}. 102 */ 103 private Lexer lexer; 104 105 /** 106 * This is the backing field for {@link #getParser()}. 107 */ 108 private Parser parser; 109 110 protected string start = "<"; 111 112 protected string stop = ">"; 113 114 /** 115 * e.g., \< and \> must escape BOTH! 116 */ 117 protected string escape = "\\"; 118 119 /** 120 * Constructs a {@link ParseTreePatternMatcher} or from a {@link Lexer} and 121 * {@link Parser} object. The lexer input stream is altered for tokenizing 122 * the tree patterns. The parser is used as a convenient mechanism to get 123 * the grammar name, plus token, rule names. 124 */ 125 public this(Lexer lexer, Parser parser) 126 { 127 this.lexer = lexer; 128 this.parser = parser; 129 } 130 131 /** 132 * Set the delimiters used for marking rule and token tags within concrete 133 * syntax used by the tree pattern parser. 134 * 135 * @param start The start delimiter. 136 * @param stop The stop delimiter. 137 * @param escapeLeft The escape sequence to use for escaping a start or stop delimiter. 138 * 139 * @exception IllegalArgumentException if {@code start} is {@code null} or empty. 140 * @exception IllegalArgumentException if {@code stop} is {@code null} or empty. 141 */ 142 public void setDelimiters(string start, string stop, string escapeLeft) 143 { 144 if (start is null || start.length) { 145 throw new IllegalArgumentException("start cannot be null or empty"); 146 } 147 148 if (stop is null || stop.length) { 149 throw new IllegalArgumentException("stop cannot be null or empty"); 150 } 151 152 this.start = start; 153 this.stop = stop; 154 this.escape = escapeLeft; 155 } 156 157 /** 158 * Does {@code pattern} matched as rule {@code patternRuleIndex} match {@code tree}? 159 */ 160 public bool matches(ParseTree tree, string pattern, int patternRuleIndex) 161 { 162 ParseTreePattern p = compile(pattern, patternRuleIndex); 163 return matches(tree, p); 164 } 165 166 /** 167 * Does {@code pattern} matched as rule patternRuleIndex match tree? Pass in a 168 * compiled pattern instead of a string representation of a tree pattern. 169 */ 170 public bool matches(ParseTree tree, ParseTreePattern pattern) 171 { 172 ParseTree[][string] labels; 173 ParseTree mismatchedNode = matchImpl(tree, pattern.getPatternTree(), labels); 174 return mismatchedNode is null; 175 } 176 177 /** 178 * Compare {@code pattern} matched as rule {@code patternRuleIndex} against 179 * {@code tree} and return a {@link ParseTreeMatch} object that contains the 180 * matched elements, or the node at which the match failed. 181 */ 182 public ParseTreeMatch match(ParseTree tree, string pattern, int patternRuleIndex) 183 { 184 ParseTreePattern p = compile(pattern, patternRuleIndex); 185 return match(tree, p); 186 } 187 188 /** 189 * Compare {@code pattern} matched against {@code tree} and return a 190 * {@link ParseTreeMatch} object that contains the matched elements, or thenode at which the match failed. Pass in a compiled pattern instead of a 191 * string representation of a tree pattern. 192 */ 193 public ParseTreeMatch match(ParseTree tree, ParseTreePattern pattern) 194 { 195 ParseTree[][string] labels; 196 ParseTree mismatchedNode = matchImpl(tree, pattern.getPatternTree(), labels); 197 return new ParseTreeMatch(tree, pattern, labels, mismatchedNode); 198 } 199 200 /** 201 * For repeated use of a tree pattern, compile it to a 202 * {@link ParseTreePattern} using this method. 203 */ 204 public ParseTreePattern compile(string pattern, int patternRuleIndex) 205 { 206 auto tokenList = tokenize(pattern); 207 ListTokenSource tokenSrc = new ListTokenSource(tokenList); 208 CommonTokenStream tokens = new CommonTokenStream(tokenSrc); 209 210 ParserInterpreter parserInterp = new ParserInterpreter(parser.getGrammarFileName(), 211 parser.getVocabulary(), 212 parser.getRuleNames(), 213 parser.getATNWithBypassAlts(), 214 tokens); 215 216 ParseTree tree = null; 217 try { 218 parserInterp.setErrorHandler(new BailErrorStrategy()); 219 tree = parserInterp.parse(patternRuleIndex); 220 // System.out.println("pattern tree = "+tree.toStringTree(parserInterp)); 221 } 222 catch (ParseCancellationException e) { 223 throw cast(RecognitionException)e.getCause(); 224 } 225 catch (RecognitionException re) { 226 throw re; 227 } 228 catch (Exception e) { 229 throw new CannotInvokeStartRule(e); 230 } 231 232 // Make sure tree pattern compilation checks for a complete parse 233 if ( tokens.LA(1)!=TokenConstantDefinition.EOF ) { 234 throw new StartRuleDoesNotConsumeFullPattern(); 235 } 236 return new ParseTreePattern(this, pattern, patternRuleIndex, tree); 237 } 238 239 /** 240 * Used to convert the tree pattern string into a series of tokens. The 241 * input stream is reset. 242 */ 243 public Lexer getLexer() 244 { 245 return lexer; 246 } 247 248 /** 249 * Used to collect to the grammar file name, token names, rule names for 250 * used to parse the pattern into a parse tree. 251 */ 252 public Parser getParser() 253 { 254 return parser; 255 } 256 257 /** 258 * Recursively walk {@code tree} against {@code patternTree}, filling 259 * {@code match.}{@link ParseTreeMatch#labels labels}. 260 * 261 * @return the first node encountered in {@code tree} which does not match 262 * a corresponding node in {@code patternTree}, or {@code null} if the match 263 * was successful. The specific node returned depends on the matching 264 * algorithm used by the implementation, and may be overridden. 265 */ 266 protected ParseTree matchImpl(ParseTree tree, ParseTree patternTree, ParseTree[][string] labels) 267 { 268 if (tree is null) { 269 throw new IllegalArgumentException("tree cannot be null"); 270 } 271 272 if (patternTree is null) { 273 throw new IllegalArgumentException("patternTree cannot be null"); 274 } 275 276 // x and <ID>, x and y, or x and x; or could be mismatched types 277 if (tree.classinfo == TerminalNode.classinfo && patternTree.classinfo == TerminalNode.classinfo) { 278 TerminalNode t1 = cast(TerminalNode)tree; 279 TerminalNode t2 = cast(TerminalNode)patternTree; 280 ParseTree mismatchedNode = null; 281 // both are tokens and they have same type 282 if (t1.getSymbol().getType() == t2.getSymbol().getType() ) { 283 if (t2.getSymbol().classinfo == TokenTagToken.classinfo) { // x and <ID> 284 TokenTagToken tokenTagToken = cast(TokenTagToken)t2.getSymbol(); 285 // track label->list-of-nodes for both token name and label (if any) 286 labels[tokenTagToken.getTokenName] ~= tree; 287 if (tokenTagToken.getLabel() !is null) { 288 labels[tokenTagToken.getLabel] ~= tree; 289 } 290 } 291 else if (t1.getText == t2.getText) { 292 // x and x 293 } 294 else { 295 // x and y 296 if (mismatchedNode is null) { 297 mismatchedNode = t1; 298 } 299 } 300 } 301 else { 302 if (mismatchedNode is null) { 303 mismatchedNode = t1; 304 } 305 } 306 307 return mismatchedNode; 308 } 309 if (tree.classinfo == ParserRuleContext.classinfo && patternTree.classinfo == ParserRuleContext.classinfo) { 310 ParserRuleContext r1 = cast(ParserRuleContext)tree; 311 ParserRuleContext r2 = cast(ParserRuleContext)patternTree; 312 ParseTree mismatchedNode = null; 313 // (expr ...) and <expr> 314 RuleTagToken ruleTagToken = getRuleTagToken(r2); 315 if ( ruleTagToken !is null ) { 316 ParseTreeMatch m = null; 317 if (r1.getRuleIndex() == r2.getRuleIndex()) { 318 // track label->list-of-nodes for both rule name and label (if any) 319 labels[ruleTagToken.getRuleName] ~= tree; 320 if ( ruleTagToken.getLabel() !is null ) { 321 labels[ruleTagToken.getLabel] ~= tree; 322 } 323 } 324 else { 325 if (mismatchedNode is null) { 326 mismatchedNode = r1; 327 } 328 } 329 330 return mismatchedNode; 331 } 332 333 // (expr ...) and (expr ...) 334 if (r1.getChildCount() != r2.getChildCount()) { 335 if (mismatchedNode is null) { 336 mismatchedNode = r1; 337 } 338 339 return mismatchedNode; 340 } 341 342 int n = r1.getChildCount(); 343 for (int i = 0; i<n; i++) { 344 ParseTree childMatch = matchImpl(r1.getChild(i), patternTree.getChild(i), labels); 345 if ( childMatch !is null ) { 346 return childMatch; 347 } 348 } 349 350 return mismatchedNode; 351 } 352 353 // if nodes aren't both tokens or both rule nodes, can't match 354 return tree; 355 } 356 357 public RuleTagToken getRuleTagToken(ParseTree t) 358 { 359 if (t.classinfo == RuleNode.classinfo) { 360 RuleNode r = cast(RuleNode)t; 361 if (r.getChildCount == 1 && r.getChild(0).classinfo == TerminalNode.classinfo) { 362 TerminalNode c = cast(TerminalNode)r.getChild(0); 363 if (c.getSymbol().classinfo == RuleTagToken.classinfo) { 364 // System.out.println("rule tag subtree "+t.toStringTree(parser)); 365 return cast(RuleTagToken)c.getSymbol(); 366 } 367 } 368 } 369 return null; 370 } 371 372 public Token[] tokenize(string pattern) 373 { 374 // split pattern into chunks: sea (raw input) and islands (<ID>, <expr>) 375 Chunk[] chunks = split(pattern); 376 377 // create token stream from text and tags 378 Token[] tokens; 379 foreach (Chunk chunk; chunks) { 380 if (chunk.classinfo == TagChunk.classinfo) { 381 TagChunk tagChunk = cast(TagChunk)chunk; 382 // add special rule token or conjure up new token from name 383 if (isUpper(tagChunk.getTag()[0])) { 384 int ttype = parser.getTokenType(tagChunk.getTag()); 385 if (ttype == TokenConstantDefinition.INVALID_TYPE ) { 386 throw new IllegalArgumentException("Unknown token " ~ tagChunk.getTag() ~ " in pattern: " ~ pattern); 387 } 388 TokenTagToken t = new TokenTagToken(tagChunk.getTag(), ttype, tagChunk.getLabel()); 389 tokens ~= t; 390 } 391 else if (isLower(tagChunk.getTag()[0]) ) { 392 int ruleIndex = parser.getRuleIndex(tagChunk.getTag()); 393 if (ruleIndex == -1) { 394 throw new IllegalArgumentException("Unknown rule " ~ tagChunk.getTag() ~ " in pattern: " ~ pattern); 395 } 396 int ruleImaginaryTokenType = parser.getATNWithBypassAlts().ruleToTokenType[ruleIndex]; 397 tokens ~= new RuleTagToken(tagChunk.getTag(), ruleImaginaryTokenType, tagChunk.getLabel()); 398 } 399 else { 400 throw new IllegalArgumentException("invalid tag: " ~ tagChunk.getTag ~ " in pattern: " ~ pattern); 401 } 402 } 403 else { 404 TextChunk textChunk = cast(TextChunk)chunk; 405 ANTLRInputStream ins = new ANTLRInputStream(textChunk.getText()); 406 lexer.setInputStream(ins); 407 Token t = lexer.nextToken(); 408 while (t.getType() != TokenConstantDefinition.EOF) { 409 tokens ~= t; 410 t = lexer.nextToken(); 411 } 412 } 413 } 414 415 // System.out.println("tokens="+tokens); 416 return tokens; 417 } 418 419 /** 420 * Split {@code <ID> = <e:expr> ;} into 4 chunks for tokenizing by {@link #tokenize}. 421 */ 422 public Chunk[] split(string pattern) 423 { 424 int p = 0; 425 int n = to!int(pattern.length); 426 Chunk[] chunks; 427 // find all start and stop indexes first, then collect 428 int[] starts; 429 int[] stops; 430 while (p < n) { 431 if (p == pattern.indexOf(escape ~ start, p) ) { 432 p += to!int(escape.length + start.length); 433 } 434 else if ( p == pattern.indexOf(escape ~ stop, p) ) { 435 p += to!int(escape.length + stop.length); 436 } 437 else if ( p == pattern.indexOf(start,p) ) { 438 starts ~= p; 439 p += to!int(start.length); 440 } 441 else if ( p == pattern.indexOf(stop,p) ) { 442 stops ~= p; 443 p += to!int(stop.length); 444 } 445 else { 446 p++; 447 } 448 } 449 450 // System.out.println(""); 451 // System.out.println(starts); 452 // System.out.println(stops); 453 if (starts.length > stops.length) { 454 throw new IllegalArgumentException("unterminated tag in pattern: " ~ pattern); 455 } 456 457 if ( starts.length < stops.length) { 458 throw new IllegalArgumentException("missing start tag in pattern: " ~ pattern); 459 } 460 461 int ntags = to!int(starts.length); 462 for (int i=0; i<ntags; i++) { 463 if ( starts[i] >= stops[i] ) { 464 throw new IllegalArgumentException("tag delimiters out of order in pattern: " ~ pattern); 465 } 466 } 467 468 // collect into chunks now 469 if (ntags == 0) { 470 string text = pattern[0.. n]; 471 chunks ~= new TextChunk(text); 472 } 473 474 if (ntags>0 && starts[0] > 0) { // copy text up to first tag into chunks 475 string text = pattern[0..starts[0]]; 476 chunks ~= new TextChunk(text); 477 } 478 for (int i=0; i<ntags; i++) { 479 // copy inside of <tag> 480 string tag = pattern[starts[i] + start.length..stops[i]]; 481 string ruleOrToken = tag; 482 string label = null; 483 int colon = to!int(tag.indexOf(':')); 484 if (colon >= 0) { 485 label = tag[0..colon]; 486 ruleOrToken = tag[colon+1..tag.length]; 487 } 488 chunks ~= new TagChunk(label, ruleOrToken); 489 if (i + 1 < ntags) { 490 // copy from end of <tag> to start of next 491 string text = pattern[stops[i] + to!int(stop.length)..starts[i + 1]]; 492 chunks ~= new TextChunk(text); 493 } 494 } 495 if ( ntags > 0 ) { 496 int afterLastTag = to!int(stops[ntags - 1] + stop.length); 497 if ( afterLastTag < n ) { // copy text from end of last tag to end 498 string text = pattern[afterLastTag..n]; 499 chunks ~= new TextChunk(text); 500 } 501 } 502 503 // strip out the escape sequences from text chunks but not tags 504 for (int i = 0; i < chunks.length; i++) { 505 Chunk c = chunks[i]; 506 if (c.classinfo == TextChunk.classinfo) { 507 TextChunk tc = cast(TextChunk)c; 508 string unescaped = tc.getText().replace(escape, ""); 509 if (unescaped.length < tc.getText().length) { 510 chunks[i] = new TextChunk(unescaped); 511 } 512 } 513 } 514 515 return chunks; 516 } 517 518 }