Checker.java revision 3255:7a0c34355149
1/* 2 * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package com.sun.tools.doclint; 27 28import java.io.IOException; 29import java.io.StringWriter; 30import java.net.URI; 31import java.net.URISyntaxException; 32import java.util.Deque; 33import java.util.EnumSet; 34import java.util.HashMap; 35import java.util.HashSet; 36import java.util.LinkedList; 37import java.util.List; 38import java.util.Map; 39import java.util.Set; 40import java.util.regex.Matcher; 41import java.util.regex.Pattern; 42 43import javax.lang.model.element.Element; 44import javax.lang.model.element.ElementKind; 45import javax.lang.model.element.ExecutableElement; 46import javax.lang.model.element.Name; 47import javax.lang.model.element.VariableElement; 48import javax.lang.model.type.TypeKind; 49import javax.lang.model.type.TypeMirror; 50import javax.tools.Diagnostic.Kind; 51import javax.tools.JavaFileObject; 52 53import com.sun.source.doctree.AttributeTree; 54import com.sun.source.doctree.AuthorTree; 55import com.sun.source.doctree.DocCommentTree; 56import com.sun.source.doctree.DocRootTree; 57import com.sun.source.doctree.DocTree; 58import com.sun.source.doctree.EndElementTree; 59import com.sun.source.doctree.EntityTree; 60import com.sun.source.doctree.ErroneousTree; 61import com.sun.source.doctree.IdentifierTree; 62import com.sun.source.doctree.InheritDocTree; 63import com.sun.source.doctree.LinkTree; 64import com.sun.source.doctree.LiteralTree; 65import com.sun.source.doctree.ParamTree; 66import com.sun.source.doctree.ReferenceTree; 67import com.sun.source.doctree.ReturnTree; 68import com.sun.source.doctree.SerialDataTree; 69import com.sun.source.doctree.SerialFieldTree; 70import com.sun.source.doctree.SinceTree; 71import com.sun.source.doctree.StartElementTree; 72import com.sun.source.doctree.TextTree; 73import com.sun.source.doctree.ThrowsTree; 74import com.sun.source.doctree.UnknownBlockTagTree; 75import com.sun.source.doctree.UnknownInlineTagTree; 76import com.sun.source.doctree.ValueTree; 77import com.sun.source.doctree.VersionTree; 78import com.sun.source.tree.Tree; 79import com.sun.source.util.DocTreePath; 80import com.sun.source.util.DocTreePathScanner; 81import com.sun.source.util.TreePath; 82import com.sun.tools.doclint.HtmlTag.AttrKind; 83import com.sun.tools.javac.tree.DocPretty; 84import com.sun.tools.javac.util.Assert; 85import com.sun.tools.javac.util.DefinedBy; 86import com.sun.tools.javac.util.DefinedBy.Api; 87import com.sun.tools.javac.util.StringUtils; 88import static com.sun.tools.doclint.Messages.Group.*; 89 90 91/** 92 * Validate a doc comment. 93 * 94 * <p><b>This is NOT part of any supported API. 95 * If you write code that depends on this, you do so at your own 96 * risk. This code and its internal interfaces are subject to change 97 * or deletion without notice.</b></p> 98 */ 99public class Checker extends DocTreePathScanner<Void, Void> { 100 final Env env; 101 102 Set<Element> foundParams = new HashSet<>(); 103 Set<TypeMirror> foundThrows = new HashSet<>(); 104 Map<Element, Set<String>> foundAnchors = new HashMap<>(); 105 boolean foundInheritDoc = false; 106 boolean foundReturn = false; 107 108 public enum Flag { 109 TABLE_HAS_CAPTION, 110 HAS_ELEMENT, 111 HAS_HEADING, 112 HAS_INLINE_TAG, 113 HAS_TEXT, 114 REPORTED_BAD_INLINE 115 } 116 117 static class TagStackItem { 118 final DocTree tree; // typically, but not always, StartElementTree 119 final HtmlTag tag; 120 final Set<HtmlTag.Attr> attrs; 121 final Set<Flag> flags; 122 TagStackItem(DocTree tree, HtmlTag tag) { 123 this.tree = tree; 124 this.tag = tag; 125 attrs = EnumSet.noneOf(HtmlTag.Attr.class); 126 flags = EnumSet.noneOf(Flag.class); 127 } 128 @Override 129 public String toString() { 130 return String.valueOf(tag); 131 } 132 } 133 134 private Deque<TagStackItem> tagStack; // TODO: maybe want to record starting tree as well 135 private HtmlTag currHeaderTag; 136 137 private final int implicitHeaderLevel; 138 139 // <editor-fold defaultstate="collapsed" desc="Top level"> 140 141 Checker(Env env) { 142 this.env = Assert.checkNonNull(env); 143 tagStack = new LinkedList<>(); 144 implicitHeaderLevel = env.implicitHeaderLevel; 145 } 146 147 public Void scan(DocCommentTree tree, TreePath p) { 148 env.initTypes(); 149 env.setCurrent(p, tree); 150 151 boolean isOverridingMethod = !env.currOverriddenMethods.isEmpty(); 152 JavaFileObject fo = p.getCompilationUnit().getSourceFile(); 153 154 if (p.getLeaf().getKind() == Tree.Kind.PACKAGE) { 155 // If p points to a package, the implied declaration is the 156 // package declaration (if any) for the compilation unit. 157 // Handle this case specially, because doc comments are only 158 // expected in package-info files. 159 boolean isPkgInfo = fo.isNameCompatible("package-info", JavaFileObject.Kind.SOURCE); 160 if (tree == null) { 161 if (isPkgInfo) 162 reportMissing("dc.missing.comment"); 163 return null; 164 } else { 165 if (!isPkgInfo) 166 reportReference("dc.unexpected.comment"); 167 } 168 } else if (tree != null && fo.isNameCompatible("package", JavaFileObject.Kind.HTML)) { 169 // a package.html file with a DocCommentTree 170 if (tree.getFullBody().isEmpty()) { 171 reportMissing("dc.missing.comment"); 172 return null; 173 } 174 } else { 175 if (tree == null) { 176 if (!isSynthetic() && !isOverridingMethod) 177 reportMissing("dc.missing.comment"); 178 return null; 179 } 180 } 181 182 tagStack.clear(); 183 currHeaderTag = null; 184 185 foundParams.clear(); 186 foundThrows.clear(); 187 foundInheritDoc = false; 188 foundReturn = false; 189 190 scan(new DocTreePath(p, tree), null); 191 192 if (!isOverridingMethod) { 193 switch (env.currElement.getKind()) { 194 case METHOD: 195 case CONSTRUCTOR: { 196 ExecutableElement ee = (ExecutableElement) env.currElement; 197 checkParamsDocumented(ee.getTypeParameters()); 198 checkParamsDocumented(ee.getParameters()); 199 switch (ee.getReturnType().getKind()) { 200 case VOID: 201 case NONE: 202 break; 203 default: 204 if (!foundReturn 205 && !foundInheritDoc 206 && !env.types.isSameType(ee.getReturnType(), env.java_lang_Void)) { 207 reportMissing("dc.missing.return"); 208 } 209 } 210 checkThrowsDocumented(ee.getThrownTypes()); 211 } 212 } 213 } 214 215 return null; 216 } 217 218 private void reportMissing(String code, Object... args) { 219 env.messages.report(MISSING, Kind.WARNING, env.currPath.getLeaf(), code, args); 220 } 221 222 private void reportReference(String code, Object... args) { 223 env.messages.report(REFERENCE, Kind.WARNING, env.currPath.getLeaf(), code, args); 224 } 225 226 @Override @DefinedBy(Api.COMPILER_TREE) 227 public Void visitDocComment(DocCommentTree tree, Void ignore) { 228 super.visitDocComment(tree, ignore); 229 for (TagStackItem tsi: tagStack) { 230 warnIfEmpty(tsi, null); 231 if (tsi.tree.getKind() == DocTree.Kind.START_ELEMENT 232 && tsi.tag.endKind == HtmlTag.EndKind.REQUIRED) { 233 StartElementTree t = (StartElementTree) tsi.tree; 234 env.messages.error(HTML, t, "dc.tag.not.closed", t.getName()); 235 } 236 } 237 return null; 238 } 239 // </editor-fold> 240 241 // <editor-fold defaultstate="collapsed" desc="Text and entities."> 242 243 @Override @DefinedBy(Api.COMPILER_TREE) 244 public Void visitText(TextTree tree, Void ignore) { 245 if (hasNonWhitespace(tree)) { 246 checkAllowsText(tree); 247 markEnclosingTag(Flag.HAS_TEXT); 248 } 249 return null; 250 } 251 252 @Override @DefinedBy(Api.COMPILER_TREE) 253 public Void visitEntity(EntityTree tree, Void ignore) { 254 checkAllowsText(tree); 255 markEnclosingTag(Flag.HAS_TEXT); 256 String name = tree.getName().toString(); 257 if (name.startsWith("#")) { 258 int v = StringUtils.toLowerCase(name).startsWith("#x") 259 ? Integer.parseInt(name.substring(2), 16) 260 : Integer.parseInt(name.substring(1), 10); 261 if (!Entity.isValid(v)) { 262 env.messages.error(HTML, tree, "dc.entity.invalid", name); 263 } 264 } else if (!Entity.isValid(name)) { 265 env.messages.error(HTML, tree, "dc.entity.invalid", name); 266 } 267 return null; 268 } 269 270 void checkAllowsText(DocTree tree) { 271 TagStackItem top = tagStack.peek(); 272 if (top != null 273 && top.tree.getKind() == DocTree.Kind.START_ELEMENT 274 && !top.tag.acceptsText()) { 275 if (top.flags.add(Flag.REPORTED_BAD_INLINE)) { 276 env.messages.error(HTML, tree, "dc.text.not.allowed", 277 ((StartElementTree) top.tree).getName()); 278 } 279 } 280 } 281 282 // </editor-fold> 283 284 // <editor-fold defaultstate="collapsed" desc="HTML elements"> 285 286 @Override @DefinedBy(Api.COMPILER_TREE) 287 public Void visitStartElement(StartElementTree tree, Void ignore) { 288 final Name treeName = tree.getName(); 289 final HtmlTag t = HtmlTag.get(treeName); 290 if (t == null) { 291 env.messages.error(HTML, tree, "dc.tag.unknown", treeName); 292 } else if (t.allowedVersion != HtmlVersion.ALL && t.allowedVersion != env.htmlVersion) { 293 env.messages.error(HTML, tree, "dc.tag.not.supported", treeName); 294 } else { 295 boolean done = false; 296 for (TagStackItem tsi: tagStack) { 297 if (tsi.tag.accepts(t)) { 298 while (tagStack.peek() != tsi) { 299 warnIfEmpty(tagStack.peek(), null); 300 tagStack.pop(); 301 } 302 done = true; 303 break; 304 } else if (tsi.tag.endKind != HtmlTag.EndKind.OPTIONAL) { 305 done = true; 306 break; 307 } 308 } 309 if (!done && HtmlTag.BODY.accepts(t)) { 310 while (!tagStack.isEmpty()) { 311 warnIfEmpty(tagStack.peek(), null); 312 tagStack.pop(); 313 } 314 } 315 316 markEnclosingTag(Flag.HAS_ELEMENT); 317 checkStructure(tree, t); 318 319 // tag specific checks 320 switch (t) { 321 // check for out of sequence headers, such as <h1>...</h1> <h3>...</h3> 322 case H1: case H2: case H3: case H4: case H5: case H6: 323 checkHeader(tree, t); 324 break; 325 } 326 327 if (t.flags.contains(HtmlTag.Flag.NO_NEST)) { 328 for (TagStackItem i: tagStack) { 329 if (t == i.tag) { 330 env.messages.warning(HTML, tree, "dc.tag.nested.not.allowed", treeName); 331 break; 332 } 333 } 334 } 335 } 336 337 // check for self closing tags, such as <a id="name"/> 338 if (tree.isSelfClosing()) { 339 env.messages.error(HTML, tree, "dc.tag.self.closing", treeName); 340 } 341 342 try { 343 TagStackItem parent = tagStack.peek(); 344 TagStackItem top = new TagStackItem(tree, t); 345 tagStack.push(top); 346 347 super.visitStartElement(tree, ignore); 348 349 // handle attributes that may or may not have been found in start element 350 if (t != null) { 351 switch (t) { 352 case CAPTION: 353 if (parent != null && parent.tag == HtmlTag.TABLE) 354 parent.flags.add(Flag.TABLE_HAS_CAPTION); 355 break; 356 357 case H1: case H2: case H3: case H4: case H5: case H6: 358 if (parent != null && (parent.tag == HtmlTag.SECTION || parent.tag == HtmlTag.ARTICLE)) { 359 parent.flags.add(Flag.HAS_HEADING); 360 } 361 break; 362 363 case IMG: 364 if (!top.attrs.contains(HtmlTag.Attr.ALT)) 365 env.messages.error(ACCESSIBILITY, tree, "dc.no.alt.attr.for.image"); 366 break; 367 } 368 } 369 370 return null; 371 } finally { 372 373 if (t == null || t.endKind == HtmlTag.EndKind.NONE) 374 tagStack.pop(); 375 } 376 } 377 378 private void checkStructure(StartElementTree tree, HtmlTag t) { 379 Name treeName = tree.getName(); 380 TagStackItem top = tagStack.peek(); 381 switch (t.blockType) { 382 case BLOCK: 383 if (top == null || top.tag.accepts(t)) 384 return; 385 386 switch (top.tree.getKind()) { 387 case START_ELEMENT: { 388 if (top.tag.blockType == HtmlTag.BlockType.INLINE) { 389 Name name = ((StartElementTree) top.tree).getName(); 390 env.messages.error(HTML, tree, "dc.tag.not.allowed.inline.element", 391 treeName, name); 392 return; 393 } 394 } 395 break; 396 397 case LINK: 398 case LINK_PLAIN: { 399 String name = top.tree.getKind().tagName; 400 env.messages.error(HTML, tree, "dc.tag.not.allowed.inline.tag", 401 treeName, name); 402 return; 403 } 404 } 405 break; 406 407 case INLINE: 408 if (top == null || top.tag.accepts(t)) 409 return; 410 break; 411 412 case LIST_ITEM: 413 case TABLE_ITEM: 414 if (top != null) { 415 // reset this flag so subsequent bad inline content gets reported 416 top.flags.remove(Flag.REPORTED_BAD_INLINE); 417 if (top.tag.accepts(t)) 418 return; 419 } 420 break; 421 422 case OTHER: 423 env.messages.error(HTML, tree, "dc.tag.not.allowed", treeName); 424 return; 425 } 426 427 env.messages.error(HTML, tree, "dc.tag.not.allowed.here", treeName); 428 } 429 430 private void checkHeader(StartElementTree tree, HtmlTag tag) { 431 // verify the new tag 432 if (getHeaderLevel(tag) > getHeaderLevel(currHeaderTag) + 1) { 433 if (currHeaderTag == null) { 434 env.messages.error(ACCESSIBILITY, tree, "dc.tag.header.sequence.1", tag); 435 } else { 436 env.messages.error(ACCESSIBILITY, tree, "dc.tag.header.sequence.2", 437 tag, currHeaderTag); 438 } 439 } 440 441 currHeaderTag = tag; 442 } 443 444 private int getHeaderLevel(HtmlTag tag) { 445 if (tag == null) 446 return implicitHeaderLevel; 447 switch (tag) { 448 case H1: return 1; 449 case H2: return 2; 450 case H3: return 3; 451 case H4: return 4; 452 case H5: return 5; 453 case H6: return 6; 454 default: throw new IllegalArgumentException(); 455 } 456 } 457 458 @Override @DefinedBy(Api.COMPILER_TREE) 459 public Void visitEndElement(EndElementTree tree, Void ignore) { 460 final Name treeName = tree.getName(); 461 final HtmlTag t = HtmlTag.get(treeName); 462 if (t == null) { 463 env.messages.error(HTML, tree, "dc.tag.unknown", treeName); 464 } else if (t.endKind == HtmlTag.EndKind.NONE) { 465 env.messages.error(HTML, tree, "dc.tag.end.not.permitted", treeName); 466 } else { 467 boolean done = false; 468 while (!tagStack.isEmpty()) { 469 TagStackItem top = tagStack.peek(); 470 if (t == top.tag) { 471 switch (t) { 472 case TABLE: 473 if (!top.attrs.contains(HtmlTag.Attr.SUMMARY) 474 && !top.flags.contains(Flag.TABLE_HAS_CAPTION)) { 475 env.messages.error(ACCESSIBILITY, tree, 476 "dc.no.summary.or.caption.for.table"); 477 } 478 break; 479 480 case SECTION: 481 case ARTICLE: 482 if (env.htmlVersion == HtmlVersion.HTML5 && !top.flags.contains(Flag.HAS_HEADING)) { 483 env.messages.error(HTML, tree, "dc.tag.requires.heading", treeName); 484 } 485 break; 486 } 487 warnIfEmpty(top, tree); 488 tagStack.pop(); 489 done = true; 490 break; 491 } else if (top.tag == null || top.tag.endKind != HtmlTag.EndKind.REQUIRED) { 492 tagStack.pop(); 493 } else { 494 boolean found = false; 495 for (TagStackItem si: tagStack) { 496 if (si.tag == t) { 497 found = true; 498 break; 499 } 500 } 501 if (found && top.tree.getKind() == DocTree.Kind.START_ELEMENT) { 502 env.messages.error(HTML, top.tree, "dc.tag.start.unmatched", 503 ((StartElementTree) top.tree).getName()); 504 tagStack.pop(); 505 } else { 506 env.messages.error(HTML, tree, "dc.tag.end.unexpected", treeName); 507 done = true; 508 break; 509 } 510 } 511 } 512 513 if (!done && tagStack.isEmpty()) { 514 env.messages.error(HTML, tree, "dc.tag.end.unexpected", treeName); 515 } 516 } 517 518 return super.visitEndElement(tree, ignore); 519 } 520 521 void warnIfEmpty(TagStackItem tsi, DocTree endTree) { 522 if (tsi.tag != null && tsi.tree instanceof StartElementTree) { 523 if (tsi.tag.flags.contains(HtmlTag.Flag.EXPECT_CONTENT) 524 && !tsi.flags.contains(Flag.HAS_TEXT) 525 && !tsi.flags.contains(Flag.HAS_ELEMENT) 526 && !tsi.flags.contains(Flag.HAS_INLINE_TAG)) { 527 DocTree tree = (endTree != null) ? endTree : tsi.tree; 528 Name treeName = ((StartElementTree) tsi.tree).getName(); 529 env.messages.warning(HTML, tree, "dc.tag.empty", treeName); 530 } 531 } 532 } 533 534 // </editor-fold> 535 536 // <editor-fold defaultstate="collapsed" desc="HTML attributes"> 537 538 @Override @DefinedBy(Api.COMPILER_TREE) @SuppressWarnings("fallthrough") 539 public Void visitAttribute(AttributeTree tree, Void ignore) { 540 HtmlTag currTag = tagStack.peek().tag; 541 if (currTag != null) { 542 Name name = tree.getName(); 543 HtmlTag.Attr attr = currTag.getAttr(name); 544 if (attr != null) { 545 if (env.htmlVersion == HtmlVersion.HTML4 && attr.name().contains("-")) { 546 env.messages.error(HTML, tree, "dc.attr.not.supported.html4", name); 547 } 548 boolean first = tagStack.peek().attrs.add(attr); 549 if (!first) 550 env.messages.error(HTML, tree, "dc.attr.repeated", name); 551 } 552 AttrKind k = currTag.getAttrKind(name); 553 switch (env.htmlVersion) { 554 case HTML4: 555 validateHtml4Attrs(tree, name, k); 556 break; 557 558 case HTML5: 559 validateHtml5Attrs(tree, name, k); 560 break; 561 } 562 563 if (attr != null) { 564 switch (attr) { 565 case NAME: 566 if (currTag != HtmlTag.A) { 567 break; 568 } 569 // fallthrough 570 case ID: 571 String value = getAttrValue(tree); 572 if (value == null) { 573 env.messages.error(HTML, tree, "dc.anchor.value.missing"); 574 } else { 575 if (!validName.matcher(value).matches()) { 576 env.messages.error(HTML, tree, "dc.invalid.anchor", value); 577 } 578 if (!checkAnchor(value)) { 579 env.messages.error(HTML, tree, "dc.anchor.already.defined", value); 580 } 581 } 582 break; 583 584 case HREF: 585 if (currTag == HtmlTag.A) { 586 String v = getAttrValue(tree); 587 if (v == null || v.isEmpty()) { 588 env.messages.error(HTML, tree, "dc.attr.lacks.value"); 589 } else { 590 Matcher m = docRoot.matcher(v); 591 if (m.matches()) { 592 String rest = m.group(2); 593 if (!rest.isEmpty()) 594 checkURI(tree, rest); 595 } else { 596 checkURI(tree, v); 597 } 598 } 599 } 600 break; 601 602 case VALUE: 603 if (currTag == HtmlTag.LI) { 604 String v = getAttrValue(tree); 605 if (v == null || v.isEmpty()) { 606 env.messages.error(HTML, tree, "dc.attr.lacks.value"); 607 } else if (!validNumber.matcher(v).matches()) { 608 env.messages.error(HTML, tree, "dc.attr.not.number"); 609 } 610 } 611 break; 612 613 case BORDER: 614 if (currTag == HtmlTag.TABLE) { 615 String v = getAttrValue(tree); 616 try { 617 if (env.htmlVersion == HtmlVersion.HTML5 618 && (v == null || (!v.isEmpty() && Integer.parseInt(v) != 1))) { 619 env.messages.error(HTML, tree, "dc.attr.table.border.html5", attr); 620 } 621 } catch (NumberFormatException ex) { 622 env.messages.error(HTML, tree, "dc.attr.table.border.html5", attr); 623 } 624 } 625 break; 626 } 627 } 628 } 629 630 // TODO: basic check on value 631 632 return super.visitAttribute(tree, ignore); 633 } 634 635 private void validateHtml4Attrs(AttributeTree tree, Name name, AttrKind k) { 636 switch (k) { 637 case ALL: 638 case HTML4: 639 break; 640 641 case INVALID: 642 env.messages.error(HTML, tree, "dc.attr.unknown", name); 643 break; 644 645 case OBSOLETE: 646 env.messages.warning(ACCESSIBILITY, tree, "dc.attr.obsolete", name); 647 break; 648 649 case USE_CSS: 650 env.messages.warning(ACCESSIBILITY, tree, "dc.attr.obsolete.use.css", name); 651 break; 652 653 case HTML5: 654 env.messages.error(HTML, tree, "dc.attr.not.supported.html4", name); 655 break; 656 } 657 } 658 659 private void validateHtml5Attrs(AttributeTree tree, Name name, AttrKind k) { 660 switch (k) { 661 case ALL: 662 case HTML5: 663 break; 664 665 case INVALID: 666 case OBSOLETE: 667 case USE_CSS: 668 case HTML4: 669 env.messages.error(HTML, tree, "dc.attr.not.supported.html5", name); 670 break; 671 } 672 } 673 674 private boolean checkAnchor(String name) { 675 Element e = getEnclosingPackageOrClass(env.currElement); 676 if (e == null) 677 return true; 678 Set<String> set = foundAnchors.get(e); 679 if (set == null) 680 foundAnchors.put(e, set = new HashSet<>()); 681 return set.add(name); 682 } 683 684 private Element getEnclosingPackageOrClass(Element e) { 685 while (e != null) { 686 switch (e.getKind()) { 687 case CLASS: 688 case ENUM: 689 case INTERFACE: 690 case PACKAGE: 691 return e; 692 default: 693 e = e.getEnclosingElement(); 694 } 695 } 696 return e; 697 } 698 699 // http://www.w3.org/TR/html401/types.html#type-name 700 private static final Pattern validName = Pattern.compile("[A-Za-z][A-Za-z0-9-_:.]*"); 701 702 private static final Pattern validNumber = Pattern.compile("-?[0-9]+"); 703 704 // pattern to remove leading {@docRoot}/? 705 private static final Pattern docRoot = Pattern.compile("(?i)(\\{@docRoot *\\}/?)?(.*)"); 706 707 private String getAttrValue(AttributeTree tree) { 708 if (tree.getValue() == null) 709 return null; 710 711 StringWriter sw = new StringWriter(); 712 try { 713 new DocPretty(sw).print(tree.getValue()); 714 } catch (IOException e) { 715 // cannot happen 716 } 717 // ignore potential use of entities for now 718 return sw.toString(); 719 } 720 721 private void checkURI(AttributeTree tree, String uri) { 722 try { 723 URI u = new URI(uri); 724 } catch (URISyntaxException e) { 725 env.messages.error(HTML, tree, "dc.invalid.uri", uri); 726 } 727 } 728 // </editor-fold> 729 730 // <editor-fold defaultstate="collapsed" desc="javadoc tags"> 731 732 @Override @DefinedBy(Api.COMPILER_TREE) 733 public Void visitAuthor(AuthorTree tree, Void ignore) { 734 warnIfEmpty(tree, tree.getName()); 735 return super.visitAuthor(tree, ignore); 736 } 737 738 @Override @DefinedBy(Api.COMPILER_TREE) 739 public Void visitDocRoot(DocRootTree tree, Void ignore) { 740 markEnclosingTag(Flag.HAS_INLINE_TAG); 741 return super.visitDocRoot(tree, ignore); 742 } 743 744 @Override @DefinedBy(Api.COMPILER_TREE) 745 public Void visitInheritDoc(InheritDocTree tree, Void ignore) { 746 markEnclosingTag(Flag.HAS_INLINE_TAG); 747 // TODO: verify on overridden method 748 foundInheritDoc = true; 749 return super.visitInheritDoc(tree, ignore); 750 } 751 752 @Override @DefinedBy(Api.COMPILER_TREE) 753 public Void visitLink(LinkTree tree, Void ignore) { 754 markEnclosingTag(Flag.HAS_INLINE_TAG); 755 // simulate inline context on tag stack 756 HtmlTag t = (tree.getKind() == DocTree.Kind.LINK) 757 ? HtmlTag.CODE : HtmlTag.SPAN; 758 tagStack.push(new TagStackItem(tree, t)); 759 try { 760 return super.visitLink(tree, ignore); 761 } finally { 762 tagStack.pop(); 763 } 764 } 765 766 @Override @DefinedBy(Api.COMPILER_TREE) 767 public Void visitLiteral(LiteralTree tree, Void ignore) { 768 markEnclosingTag(Flag.HAS_INLINE_TAG); 769 if (tree.getKind() == DocTree.Kind.CODE) { 770 for (TagStackItem tsi: tagStack) { 771 if (tsi.tag == HtmlTag.CODE) { 772 env.messages.warning(HTML, tree, "dc.tag.code.within.code"); 773 break; 774 } 775 } 776 } 777 return super.visitLiteral(tree, ignore); 778 } 779 780 @Override @DefinedBy(Api.COMPILER_TREE) 781 @SuppressWarnings("fallthrough") 782 public Void visitParam(ParamTree tree, Void ignore) { 783 boolean typaram = tree.isTypeParameter(); 784 IdentifierTree nameTree = tree.getName(); 785 Element paramElement = nameTree != null ? env.trees.getElement(new DocTreePath(getCurrentPath(), nameTree)) : null; 786 787 if (paramElement == null) { 788 switch (env.currElement.getKind()) { 789 case CLASS: case INTERFACE: { 790 if (!typaram) { 791 env.messages.error(REFERENCE, tree, "dc.invalid.param"); 792 break; 793 } 794 } 795 case METHOD: case CONSTRUCTOR: { 796 env.messages.error(REFERENCE, nameTree, "dc.param.name.not.found"); 797 break; 798 } 799 800 default: 801 env.messages.error(REFERENCE, tree, "dc.invalid.param"); 802 break; 803 } 804 } else { 805 foundParams.add(paramElement); 806 } 807 808 warnIfEmpty(tree, tree.getDescription()); 809 return super.visitParam(tree, ignore); 810 } 811 812 private void checkParamsDocumented(List<? extends Element> list) { 813 if (foundInheritDoc) 814 return; 815 816 for (Element e: list) { 817 if (!foundParams.contains(e)) { 818 CharSequence paramName = (e.getKind() == ElementKind.TYPE_PARAMETER) 819 ? "<" + e.getSimpleName() + ">" 820 : e.getSimpleName(); 821 reportMissing("dc.missing.param", paramName); 822 } 823 } 824 } 825 826 @Override @DefinedBy(Api.COMPILER_TREE) 827 public Void visitReference(ReferenceTree tree, Void ignore) { 828 String sig = tree.getSignature(); 829 if (sig.contains("<") || sig.contains(">")) 830 env.messages.error(REFERENCE, tree, "dc.type.arg.not.allowed"); 831 832 Element e = env.trees.getElement(getCurrentPath()); 833 if (e == null) 834 env.messages.error(REFERENCE, tree, "dc.ref.not.found"); 835 return super.visitReference(tree, ignore); 836 } 837 838 @Override @DefinedBy(Api.COMPILER_TREE) 839 public Void visitReturn(ReturnTree tree, Void ignore) { 840 Element e = env.trees.getElement(env.currPath); 841 if (e.getKind() != ElementKind.METHOD 842 || ((ExecutableElement) e).getReturnType().getKind() == TypeKind.VOID) 843 env.messages.error(REFERENCE, tree, "dc.invalid.return"); 844 foundReturn = true; 845 warnIfEmpty(tree, tree.getDescription()); 846 return super.visitReturn(tree, ignore); 847 } 848 849 @Override @DefinedBy(Api.COMPILER_TREE) 850 public Void visitSerialData(SerialDataTree tree, Void ignore) { 851 warnIfEmpty(tree, tree.getDescription()); 852 return super.visitSerialData(tree, ignore); 853 } 854 855 @Override @DefinedBy(Api.COMPILER_TREE) 856 public Void visitSerialField(SerialFieldTree tree, Void ignore) { 857 warnIfEmpty(tree, tree.getDescription()); 858 return super.visitSerialField(tree, ignore); 859 } 860 861 @Override @DefinedBy(Api.COMPILER_TREE) 862 public Void visitSince(SinceTree tree, Void ignore) { 863 warnIfEmpty(tree, tree.getBody()); 864 return super.visitSince(tree, ignore); 865 } 866 867 @Override @DefinedBy(Api.COMPILER_TREE) 868 public Void visitThrows(ThrowsTree tree, Void ignore) { 869 ReferenceTree exName = tree.getExceptionName(); 870 Element ex = env.trees.getElement(new DocTreePath(getCurrentPath(), exName)); 871 if (ex == null) { 872 env.messages.error(REFERENCE, tree, "dc.ref.not.found"); 873 } else if (isThrowable(ex.asType())) { 874 switch (env.currElement.getKind()) { 875 case CONSTRUCTOR: 876 case METHOD: 877 if (isCheckedException(ex.asType())) { 878 ExecutableElement ee = (ExecutableElement) env.currElement; 879 checkThrowsDeclared(exName, ex.asType(), ee.getThrownTypes()); 880 } 881 break; 882 default: 883 env.messages.error(REFERENCE, tree, "dc.invalid.throws"); 884 } 885 } else { 886 env.messages.error(REFERENCE, tree, "dc.invalid.throws"); 887 } 888 warnIfEmpty(tree, tree.getDescription()); 889 return scan(tree.getDescription(), ignore); 890 } 891 892 private boolean isThrowable(TypeMirror tm) { 893 switch (tm.getKind()) { 894 case DECLARED: 895 case TYPEVAR: 896 return env.types.isAssignable(tm, env.java_lang_Throwable); 897 } 898 return false; 899 } 900 901 private void checkThrowsDeclared(ReferenceTree tree, TypeMirror t, List<? extends TypeMirror> list) { 902 boolean found = false; 903 for (TypeMirror tl : list) { 904 if (env.types.isAssignable(t, tl)) { 905 foundThrows.add(tl); 906 found = true; 907 } 908 } 909 if (!found) 910 env.messages.error(REFERENCE, tree, "dc.exception.not.thrown", t); 911 } 912 913 private void checkThrowsDocumented(List<? extends TypeMirror> list) { 914 if (foundInheritDoc) 915 return; 916 917 for (TypeMirror tl: list) { 918 if (isCheckedException(tl) && !foundThrows.contains(tl)) 919 reportMissing("dc.missing.throws", tl); 920 } 921 } 922 923 @Override @DefinedBy(Api.COMPILER_TREE) 924 public Void visitUnknownBlockTag(UnknownBlockTagTree tree, Void ignore) { 925 checkUnknownTag(tree, tree.getTagName()); 926 return super.visitUnknownBlockTag(tree, ignore); 927 } 928 929 @Override @DefinedBy(Api.COMPILER_TREE) 930 public Void visitUnknownInlineTag(UnknownInlineTagTree tree, Void ignore) { 931 checkUnknownTag(tree, tree.getTagName()); 932 return super.visitUnknownInlineTag(tree, ignore); 933 } 934 935 private void checkUnknownTag(DocTree tree, String tagName) { 936 if (env.customTags != null && !env.customTags.contains(tagName)) 937 env.messages.error(SYNTAX, tree, "dc.tag.unknown", tagName); 938 } 939 940 @Override @DefinedBy(Api.COMPILER_TREE) 941 public Void visitValue(ValueTree tree, Void ignore) { 942 ReferenceTree ref = tree.getReference(); 943 if (ref == null || ref.getSignature().isEmpty()) { 944 if (!isConstant(env.currElement)) 945 env.messages.error(REFERENCE, tree, "dc.value.not.allowed.here"); 946 } else { 947 Element e = env.trees.getElement(new DocTreePath(getCurrentPath(), ref)); 948 if (!isConstant(e)) 949 env.messages.error(REFERENCE, tree, "dc.value.not.a.constant"); 950 } 951 952 markEnclosingTag(Flag.HAS_INLINE_TAG); 953 return super.visitValue(tree, ignore); 954 } 955 956 private boolean isConstant(Element e) { 957 if (e == null) 958 return false; 959 960 switch (e.getKind()) { 961 case FIELD: 962 Object value = ((VariableElement) e).getConstantValue(); 963 return (value != null); // can't distinguish "not a constant" from "constant is null" 964 default: 965 return false; 966 } 967 } 968 969 @Override @DefinedBy(Api.COMPILER_TREE) 970 public Void visitVersion(VersionTree tree, Void ignore) { 971 warnIfEmpty(tree, tree.getBody()); 972 return super.visitVersion(tree, ignore); 973 } 974 975 @Override @DefinedBy(Api.COMPILER_TREE) 976 public Void visitErroneous(ErroneousTree tree, Void ignore) { 977 env.messages.error(SYNTAX, tree, null, tree.getDiagnostic().getMessage(null)); 978 return null; 979 } 980 // </editor-fold> 981 982 // <editor-fold defaultstate="collapsed" desc="Utility methods"> 983 984 private boolean isCheckedException(TypeMirror t) { 985 return !(env.types.isAssignable(t, env.java_lang_Error) 986 || env.types.isAssignable(t, env.java_lang_RuntimeException)); 987 } 988 989 private boolean isSynthetic() { 990 switch (env.currElement.getKind()) { 991 case CONSTRUCTOR: 992 // A synthetic default constructor has the same pos as the 993 // enclosing class 994 TreePath p = env.currPath; 995 return env.getPos(p) == env.getPos(p.getParentPath()); 996 } 997 return false; 998 } 999 1000 void markEnclosingTag(Flag flag) { 1001 TagStackItem top = tagStack.peek(); 1002 if (top != null) 1003 top.flags.add(flag); 1004 } 1005 1006 String toString(TreePath p) { 1007 StringBuilder sb = new StringBuilder("TreePath["); 1008 toString(p, sb); 1009 sb.append("]"); 1010 return sb.toString(); 1011 } 1012 1013 void toString(TreePath p, StringBuilder sb) { 1014 TreePath parent = p.getParentPath(); 1015 if (parent != null) { 1016 toString(parent, sb); 1017 sb.append(","); 1018 } 1019 sb.append(p.getLeaf().getKind()).append(":").append(env.getPos(p)).append(":S").append(env.getStartPos(p)); 1020 } 1021 1022 void warnIfEmpty(DocTree tree, List<? extends DocTree> list) { 1023 for (DocTree d: list) { 1024 switch (d.getKind()) { 1025 case TEXT: 1026 if (hasNonWhitespace((TextTree) d)) 1027 return; 1028 break; 1029 default: 1030 return; 1031 } 1032 } 1033 env.messages.warning(SYNTAX, tree, "dc.empty", tree.getKind().tagName); 1034 } 1035 1036 boolean hasNonWhitespace(TextTree tree) { 1037 String s = tree.getBody(); 1038 for (int i = 0; i < s.length(); i++) { 1039 if (!Character.isWhitespace(s.charAt(i))) 1040 return true; 1041 } 1042 return false; 1043 } 1044 1045 // </editor-fold> 1046 1047} 1048