1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle;
21
22 import java.util.ArrayDeque;
23 import java.util.Deque;
24 import java.util.List;
25
26 import org.antlr.v4.runtime.BaseErrorListener;
27 import org.antlr.v4.runtime.BufferedTokenStream;
28 import org.antlr.v4.runtime.CharStreams;
29 import org.antlr.v4.runtime.CommonToken;
30 import org.antlr.v4.runtime.CommonTokenStream;
31 import org.antlr.v4.runtime.FailedPredicateException;
32 import org.antlr.v4.runtime.NoViableAltException;
33 import org.antlr.v4.runtime.ParserRuleContext;
34 import org.antlr.v4.runtime.RecognitionException;
35 import org.antlr.v4.runtime.Recognizer;
36 import org.antlr.v4.runtime.Token;
37 import org.antlr.v4.runtime.atn.PredictionMode;
38 import org.antlr.v4.runtime.misc.Interval;
39 import org.antlr.v4.runtime.misc.ParseCancellationException;
40 import org.antlr.v4.runtime.tree.ParseTree;
41 import org.antlr.v4.runtime.tree.TerminalNode;
42
43 import com.puppycrawl.tools.checkstyle.api.DetailAST;
44 import com.puppycrawl.tools.checkstyle.api.DetailNode;
45 import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
46 import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
47 import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocLexer;
48 import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocParser;
49 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
50
51
52
53
54
55 public class JavadocDetailNodeParser {
56
57
58
59
60
61
62
63
64 public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
65
66
67
68
69 public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
70 "javadoc.wrong.singleton.html.tag";
71
72
73
74
75 public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
76
77
78
79
80 public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml";
81
82
83 private static final String JAVADOC_START = "/**";
84
85
86
87
88 private int blockCommentLineNumber;
89
90
91
92
93
94
95
96
97 public ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) {
98 blockCommentLineNumber = javadocCommentAst.getLineNo();
99
100 final String javadocComment = JavadocUtil.getJavadocCommentContent(javadocCommentAst);
101
102
103
104
105 final DescriptiveErrorListener errorListener = new DescriptiveErrorListener();
106
107
108
109
110 errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
111
112 final ParseStatus result = new ParseStatus();
113
114 try {
115 final JavadocParser javadocParser = createJavadocParser(javadocComment, errorListener);
116
117 final ParseTree javadocParseTree = javadocParser.javadoc();
118
119 final DetailNode tree = convertParseTreeToDetailNode(javadocParseTree);
120
121 adjustFirstLineToJavadocIndent(tree,
122 javadocCommentAst.getColumnNo()
123 + JAVADOC_START.length());
124 result.setTree(tree);
125 result.firstNonTightHtmlTag = getFirstNonTightHtmlTag(javadocParser,
126 errorListener.offset);
127 }
128 catch (ParseCancellationException | IllegalArgumentException exc) {
129 ParseErrorMessage parseErrorMessage = null;
130
131 if (exc.getCause() instanceof FailedPredicateException
132 || exc.getCause() instanceof NoViableAltException) {
133 final RecognitionException recognitionEx = (RecognitionException) exc.getCause();
134 if (recognitionEx.getCtx() instanceof JavadocParser.HtmlTagContext) {
135 final Token htmlTagNameStart = getMissedHtmlTag(recognitionEx);
136 parseErrorMessage = new ParseErrorMessage(
137 errorListener.offset + htmlTagNameStart.getLine(),
138 MSG_JAVADOC_MISSED_HTML_CLOSE,
139 htmlTagNameStart.getCharPositionInLine(),
140 htmlTagNameStart.getText());
141 }
142 }
143
144 if (parseErrorMessage == null) {
145
146
147
148 parseErrorMessage = errorListener.getErrorMessage();
149 }
150
151 result.setParseErrorMessage(parseErrorMessage);
152 }
153
154 return result;
155 }
156
157
158
159
160
161
162
163
164
165 private static JavadocParser createJavadocParser(String blockComment,
166 DescriptiveErrorListener errorListener) {
167 final JavadocLexer lexer = new JavadocLexer(CharStreams.fromString(blockComment), true);
168
169 final CommonTokenStream tokens = new CommonTokenStream(lexer);
170
171 final JavadocParser parser = new JavadocParser(tokens);
172
173
174 parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
175
176
177 parser.removeErrorListeners();
178
179
180 parser.addErrorListener(errorListener);
181
182
183
184 parser.setErrorHandler(new CheckstyleParserErrorStrategy());
185
186 return parser;
187 }
188
189
190
191
192
193
194
195
196
197
198 private DetailNode convertParseTreeToDetailNode(ParseTree parseTreeNode) {
199 final JavadocNodeImpl rootJavadocNode = createRootJavadocNode(parseTreeNode);
200
201 JavadocNodeImpl currentJavadocParent = rootJavadocNode;
202 ParseTree parseTreeParent = parseTreeNode;
203
204 while (currentJavadocParent != null) {
205
206 if (currentJavadocParent.getType() == JavadocTokenTypes.TEXT) {
207 currentJavadocParent.setChildren(JavadocNodeImpl.EMPTY_DETAIL_NODE_ARRAY);
208 }
209
210 final JavadocNodeImpl[] children =
211 (JavadocNodeImpl[]) currentJavadocParent.getChildren();
212
213 insertChildrenNodes(children, parseTreeParent);
214
215 if (children.length > 0) {
216 currentJavadocParent = children[0];
217 parseTreeParent = parseTreeParent.getChild(0);
218 }
219 else {
220 JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtil
221 .getNextSibling(currentJavadocParent);
222
223 ParseTree nextParseTreeSibling = getNextSibling(parseTreeParent);
224
225 while (nextJavadocSibling == null) {
226 currentJavadocParent =
227 (JavadocNodeImpl) currentJavadocParent.getParent();
228
229 parseTreeParent = parseTreeParent.getParent();
230
231 if (currentJavadocParent == null) {
232 break;
233 }
234
235 nextJavadocSibling = (JavadocNodeImpl) JavadocUtil
236 .getNextSibling(currentJavadocParent);
237
238 nextParseTreeSibling = getNextSibling(parseTreeParent);
239 }
240 currentJavadocParent = nextJavadocSibling;
241 parseTreeParent = nextParseTreeSibling;
242 }
243 }
244
245 return rootJavadocNode;
246 }
247
248
249
250
251
252
253
254 private void insertChildrenNodes(final JavadocNodeImpl[] nodes, ParseTree parseTreeParent) {
255 for (int i = 0; i < nodes.length; i++) {
256 final JavadocNodeImpl currentJavadocNode = nodes[i];
257 final ParseTree currentParseTreeNodeChild = parseTreeParent.getChild(i);
258 final JavadocNodeImpl[] subChildren =
259 createChildrenNodes(currentJavadocNode, currentParseTreeNodeChild);
260 currentJavadocNode.setChildren(subChildren);
261 }
262 }
263
264
265
266
267
268
269
270
271 private JavadocNodeImpl[]
272 createChildrenNodes(DetailNode parentJavadocNode, ParseTree parseTreeNode) {
273 final JavadocNodeImpl[] children =
274 new JavadocNodeImpl[parseTreeNode.getChildCount()];
275
276 for (int j = 0; j < children.length; j++) {
277 final JavadocNodeImpl child =
278 createJavadocNode(parseTreeNode.getChild(j), parentJavadocNode, j);
279
280 children[j] = child;
281 }
282 return children;
283 }
284
285
286
287
288
289
290
291 private JavadocNodeImpl createRootJavadocNode(ParseTree parseTreeNode) {
292 final JavadocNodeImpl rootJavadocNode = createJavadocNode(parseTreeNode, null, -1);
293
294 final int childCount = parseTreeNode.getChildCount();
295 final DetailNode[] children = rootJavadocNode.getChildren();
296
297 for (int i = 0; i < childCount; i++) {
298 final JavadocNodeImpl child = createJavadocNode(parseTreeNode.getChild(i),
299 rootJavadocNode, i);
300 children[i] = child;
301 }
302 rootJavadocNode.setChildren(children);
303 return rootJavadocNode;
304 }
305
306
307
308
309
310
311
312
313
314 private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index) {
315 final JavadocNodeImpl node = new JavadocNodeImpl();
316 if (parseTree.getChildCount() == 0
317 || "Text".equals(getNodeClassNameWithoutContext(parseTree))) {
318 node.setText(parseTree.getText());
319 }
320 else {
321 node.setText(getFormattedNodeClassNameWithoutContext(parseTree));
322 }
323 node.setColumnNumber(getColumn(parseTree));
324 node.setLineNumber(getLine(parseTree) + blockCommentLineNumber);
325 node.setIndex(index);
326 node.setType(getTokenType(parseTree));
327 node.setParent(parent);
328 node.setChildren(new JavadocNodeImpl[parseTree.getChildCount()]);
329 return node;
330 }
331
332
333
334
335
336
337
338 private void adjustFirstLineToJavadocIndent(DetailNode tree, int javadocColumnNumber) {
339 if (tree.getLineNumber() == blockCommentLineNumber) {
340 ((JavadocNodeImpl) tree).setColumnNumber(tree.getColumnNumber() + javadocColumnNumber);
341 final DetailNode[] children = tree.getChildren();
342 for (DetailNode child : children) {
343 adjustFirstLineToJavadocIndent(child, javadocColumnNumber);
344 }
345 }
346 }
347
348
349
350
351
352
353
354
355 private static int getLine(ParseTree tree) {
356 final int line;
357 if (tree instanceof TerminalNode) {
358 line = ((TerminalNode) tree).getSymbol().getLine() - 1;
359 }
360 else {
361 final ParserRuleContext rule = (ParserRuleContext) tree;
362 line = rule.start.getLine() - 1;
363 }
364 return line;
365 }
366
367
368
369
370
371
372
373
374 private static int getColumn(ParseTree tree) {
375 final int column;
376 if (tree instanceof TerminalNode) {
377 column = ((TerminalNode) tree).getSymbol().getCharPositionInLine();
378 }
379 else {
380 final ParserRuleContext rule = (ParserRuleContext) tree;
381 column = rule.start.getCharPositionInLine();
382 }
383 return column;
384 }
385
386
387
388
389
390
391
392 private static ParseTree getNextSibling(ParseTree node) {
393 ParseTree nextSibling = null;
394
395 if (node.getParent() != null) {
396 final ParseTree parent = node.getParent();
397 int index = 0;
398 while (true) {
399 final ParseTree currentNode = parent.getChild(index);
400 if (currentNode.equals(node)) {
401 nextSibling = parent.getChild(index + 1);
402 break;
403 }
404 index++;
405 }
406 }
407 return nextSibling;
408 }
409
410
411
412
413
414
415
416 private static int getTokenType(ParseTree node) {
417 final int tokenType;
418
419 if (node.getChildCount() == 0) {
420 tokenType = ((TerminalNode) node).getSymbol().getType();
421 }
422 else {
423 final String className = getNodeClassNameWithoutContext(node);
424 tokenType = JavadocUtil.getTokenId(convertUpperCamelToUpperUnderscore(className));
425 }
426
427 return tokenType;
428 }
429
430
431
432
433
434
435
436
437
438 private static String getFormattedNodeClassNameWithoutContext(ParseTree node) {
439 final String classNameWithoutContext = getNodeClassNameWithoutContext(node);
440 return convertUpperCamelToUpperUnderscore(classNameWithoutContext);
441 }
442
443
444
445
446
447
448
449
450
451 private static String getNodeClassNameWithoutContext(ParseTree node) {
452 final String className = node.getClass().getSimpleName();
453
454 final int contextLength = 7;
455 return className.substring(0, className.length() - contextLength);
456 }
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486 private static Token getMissedHtmlTag(RecognitionException exception) {
487 final Interval sourceInterval = exception.getCtx().getSourceInterval();
488 final List<Token> tokenList = ((BufferedTokenStream) exception.getInputStream())
489 .getTokens(sourceInterval.a, sourceInterval.b);
490 final Deque<Token> stack = new ArrayDeque<>();
491 int prevTokenType = JavadocTokenTypes.EOF;
492 for (final Token token : tokenList) {
493 final int tokenType = token.getType();
494 if (tokenType == JavadocTokenTypes.HTML_TAG_NAME
495 && prevTokenType == JavadocTokenTypes.START) {
496 stack.push(token);
497 }
498 else if (tokenType == JavadocTokenTypes.HTML_TAG_NAME
499 && stack.peek().getText().equals(token.getText())) {
500 stack.pop();
501 }
502 prevTokenType = tokenType;
503 }
504 return stack.pop();
505 }
506
507
508
509
510
511
512
513
514
515
516
517
518 private static Token getFirstNonTightHtmlTag(JavadocParser javadocParser,
519 int javadocLineOffset) {
520 final CommonToken offendingToken;
521 final ParserRuleContext nonTightTagStartContext = javadocParser.nonTightTagStartContext;
522 if (nonTightTagStartContext == null) {
523 offendingToken = null;
524 }
525 else {
526 final Token token = ((TerminalNode) nonTightTagStartContext.getChild(1))
527 .getSymbol();
528 offendingToken = new CommonToken(token);
529 offendingToken.setLine(offendingToken.getLine() + javadocLineOffset);
530 }
531 return offendingToken;
532 }
533
534
535
536
537
538
539
540
541 private static String convertUpperCamelToUpperUnderscore(String text) {
542 final StringBuilder result = new StringBuilder(20);
543 boolean first = true;
544 for (char letter : text.toCharArray()) {
545 if (!first && Character.isUpperCase(letter)) {
546 result.append('_');
547 }
548 result.append(Character.toUpperCase(letter));
549 first = false;
550 }
551 return result.toString();
552 }
553
554
555
556
557 private static final class DescriptiveErrorListener extends BaseErrorListener {
558
559
560
561
562
563
564 private int offset;
565
566
567
568
569 private ParseErrorMessage errorMessage;
570
571
572
573
574
575
576 private ParseErrorMessage getErrorMessage() {
577 return errorMessage;
578 }
579
580
581
582
583
584
585
586
587
588 public void setOffset(int offset) {
589 this.offset = offset;
590 }
591
592
593
594
595
596
597
598
599
600
601
602 @Override
603 public void syntaxError(
604 Recognizer<?, ?> recognizer, Object offendingSymbol,
605 int line, int charPositionInLine,
606 String msg, RecognitionException ex) {
607 final int lineNumber = offset + line;
608
609 if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
610 errorMessage = new ParseErrorMessage(lineNumber,
611 MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine,
612 ((Token) offendingSymbol).getText());
613
614 throw new IllegalArgumentException(msg);
615 }
616
617 final int ruleIndex = ex.getCtx().getRuleIndex();
618 final String ruleName = recognizer.getRuleNames()[ruleIndex];
619 final String upperCaseRuleName = convertUpperCamelToUpperUnderscore(ruleName);
620
621 errorMessage = new ParseErrorMessage(lineNumber,
622 MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
623
624 }
625
626 }
627
628
629
630
631
632 public static class ParseStatus {
633
634
635
636
637 private DetailNode tree;
638
639
640
641
642 private ParseErrorMessage parseErrorMessage;
643
644
645
646
647
648
649
650
651 private Token firstNonTightHtmlTag;
652
653
654
655
656
657
658 public DetailNode getTree() {
659 return tree;
660 }
661
662
663
664
665
666
667 public void setTree(DetailNode tree) {
668 this.tree = tree;
669 }
670
671
672
673
674
675
676 public ParseErrorMessage getParseErrorMessage() {
677 return parseErrorMessage;
678 }
679
680
681
682
683
684
685 public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
686 this.parseErrorMessage = parseErrorMessage;
687 }
688
689
690
691
692
693
694
695
696
697 public boolean isNonTight() {
698 return firstNonTightHtmlTag != null;
699 }
700
701
702
703
704
705
706
707
708
709 public Token getFirstNonTightHtmlTag() {
710 return firstNonTightHtmlTag;
711 }
712
713 }
714
715
716
717
718 public static class ParseErrorMessage {
719
720
721
722
723 private final int lineNumber;
724
725
726
727
728 private final String messageKey;
729
730
731
732
733 private final Object[] messageArguments;
734
735
736
737
738
739
740
741
742 ParseErrorMessage(int lineNumber, String messageKey,
743 Object... messageArguments) {
744 this.lineNumber = lineNumber;
745 this.messageKey = messageKey;
746 this.messageArguments = messageArguments.clone();
747 }
748
749
750
751
752
753
754 public int getLineNumber() {
755 return lineNumber;
756 }
757
758
759
760
761
762
763 public String getMessageKey() {
764 return messageKey;
765 }
766
767
768
769
770
771
772 public Object[] getMessageArguments() {
773 return messageArguments.clone();
774 }
775
776 }
777 }