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 ex) {
129 ParseErrorMessage parseErrorMessage = null;
130
131 if (ex.getCause() instanceof FailedPredicateException
132 || ex.getCause() instanceof NoViableAltException) {
133 final RecognitionException recognitionEx = (RecognitionException) ex.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 Token htmlTagNameStart = null;
488 final Interval sourceInterval = exception.getCtx().getSourceInterval();
489 final List<Token> tokenList = ((BufferedTokenStream) exception.getInputStream())
490 .getTokens(sourceInterval.a, sourceInterval.b);
491 final Deque<Token> stack = new ArrayDeque<>();
492 int prevTokenType = JavadocTokenTypes.EOF;
493 for (final Token token : tokenList) {
494 final int tokenType = token.getType();
495 if (tokenType == JavadocTokenTypes.HTML_TAG_NAME
496 && prevTokenType == JavadocTokenTypes.START) {
497 stack.push(token);
498 }
499 else if (tokenType == JavadocTokenTypes.HTML_TAG_NAME && !stack.isEmpty()) {
500 if (stack.peek().getText().equals(token.getText())) {
501 stack.pop();
502 }
503 else {
504 htmlTagNameStart = stack.pop();
505 }
506 }
507 prevTokenType = tokenType;
508 }
509 if (htmlTagNameStart == null) {
510 htmlTagNameStart = stack.pop();
511 }
512 return htmlTagNameStart;
513 }
514
515
516
517
518
519
520
521
522
523
524
525
526 private static Token getFirstNonTightHtmlTag(JavadocParser javadocParser,
527 int javadocLineOffset) {
528 final CommonToken offendingToken;
529 final ParserRuleContext nonTightTagStartContext = javadocParser.nonTightTagStartContext;
530 if (nonTightTagStartContext == null) {
531 offendingToken = null;
532 }
533 else {
534 final Token token = ((TerminalNode) nonTightTagStartContext.getChild(1))
535 .getSymbol();
536 offendingToken = new CommonToken(token);
537 offendingToken.setLine(offendingToken.getLine() + javadocLineOffset);
538 }
539 return offendingToken;
540 }
541
542
543
544
545
546
547
548
549 private static String convertUpperCamelToUpperUnderscore(String text) {
550 final StringBuilder result = new StringBuilder(20);
551 boolean first = true;
552 for (char letter : text.toCharArray()) {
553 if (!first && Character.isUpperCase(letter)) {
554 result.append('_');
555 }
556 result.append(Character.toUpperCase(letter));
557 first = false;
558 }
559 return result.toString();
560 }
561
562
563
564
565 private static final class DescriptiveErrorListener extends BaseErrorListener {
566
567
568
569
570
571
572 private int offset;
573
574
575
576
577 private ParseErrorMessage errorMessage;
578
579
580
581
582
583
584 private ParseErrorMessage getErrorMessage() {
585 return errorMessage;
586 }
587
588
589
590
591
592
593
594
595
596 public void setOffset(int offset) {
597 this.offset = offset;
598 }
599
600
601
602
603
604
605
606
607
608
609
610 @Override
611 public void syntaxError(
612 Recognizer<?, ?> recognizer, Object offendingSymbol,
613 int line, int charPositionInLine,
614 String msg, RecognitionException ex) {
615 final int lineNumber = offset + line;
616
617 if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
618 errorMessage = new ParseErrorMessage(lineNumber,
619 MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine,
620 ((Token) offendingSymbol).getText());
621
622 throw new IllegalArgumentException(msg);
623 }
624
625 final int ruleIndex = ex.getCtx().getRuleIndex();
626 final String ruleName = recognizer.getRuleNames()[ruleIndex];
627 final String upperCaseRuleName = convertUpperCamelToUpperUnderscore(ruleName);
628
629 errorMessage = new ParseErrorMessage(lineNumber,
630 MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
631
632 }
633
634 }
635
636
637
638
639
640 public static class ParseStatus {
641
642
643
644
645 private DetailNode tree;
646
647
648
649
650 private ParseErrorMessage parseErrorMessage;
651
652
653
654
655
656
657
658
659 private Token firstNonTightHtmlTag;
660
661
662
663
664
665
666 public DetailNode getTree() {
667 return tree;
668 }
669
670
671
672
673
674
675 public void setTree(DetailNode tree) {
676 this.tree = tree;
677 }
678
679
680
681
682
683
684 public ParseErrorMessage getParseErrorMessage() {
685 return parseErrorMessage;
686 }
687
688
689
690
691
692
693 public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
694 this.parseErrorMessage = parseErrorMessage;
695 }
696
697
698
699
700
701
702
703
704
705 public boolean isNonTight() {
706 return firstNonTightHtmlTag != null;
707 }
708
709
710
711
712
713
714
715
716
717 public Token getFirstNonTightHtmlTag() {
718 return firstNonTightHtmlTag;
719 }
720
721 }
722
723
724
725
726 public static class ParseErrorMessage {
727
728
729
730
731 private final int lineNumber;
732
733
734
735
736 private final String messageKey;
737
738
739
740
741 private final Object[] messageArguments;
742
743
744
745
746
747
748
749
750 ParseErrorMessage(int lineNumber, String messageKey,
751 Object... messageArguments) {
752 this.lineNumber = lineNumber;
753 this.messageKey = messageKey;
754 this.messageArguments = messageArguments.clone();
755 }
756
757
758
759
760
761
762 public int getLineNumber() {
763 return lineNumber;
764 }
765
766
767
768
769
770
771 public String getMessageKey() {
772 return messageKey;
773 }
774
775
776
777
778
779
780 public Object[] getMessageArguments() {
781 return messageArguments.clone();
782 }
783
784 }
785 }