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