1 /* 2 * Copyright (c) 2012, 2021, 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 26 package jdk.javadoc.internal.doclint; 27 28 import java.io.IOException; 29 import java.io.StringWriter; 30 import java.net.URI; 31 import java.net.URISyntaxException; 32 import java.util.Deque; 33 import java.util.EnumSet; 34 import java.util.HashMap; 35 import java.util.HashSet; 36 import java.util.LinkedList; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.Set; 41 import java.util.regex.Matcher; 42 import java.util.regex.Pattern; 43 44 import javax.lang.model.element.Element; 45 import javax.lang.model.element.ElementKind; 46 import javax.lang.model.element.ExecutableElement; 47 import javax.lang.model.element.Name; 48 import javax.lang.model.element.VariableElement; 49 import javax.lang.model.type.TypeKind; 50 import javax.lang.model.type.TypeMirror; 51 import javax.tools.Diagnostic.Kind; 52 import javax.tools.JavaFileObject; 53 54 import com.sun.source.doctree.AttributeTree; 55 import com.sun.source.doctree.AuthorTree; 56 import com.sun.source.doctree.DocCommentTree; 57 import com.sun.source.doctree.DocRootTree; 58 import com.sun.source.doctree.DocTree; 59 import com.sun.source.doctree.EndElementTree; 60 import com.sun.source.doctree.EntityTree; 61 import com.sun.source.doctree.ErroneousTree; 62 import com.sun.source.doctree.IdentifierTree; 63 import com.sun.source.doctree.IndexTree; 64 import com.sun.source.doctree.InheritDocTree; 65 import com.sun.source.doctree.LinkTree; 66 import com.sun.source.doctree.LiteralTree; 67 import com.sun.source.doctree.ParamTree; 68 import com.sun.source.doctree.ProvidesTree; 69 import com.sun.source.doctree.ReferenceTree; 70 import com.sun.source.doctree.ReturnTree; 71 import com.sun.source.doctree.SerialDataTree; 72 import com.sun.source.doctree.SerialFieldTree; 73 import com.sun.source.doctree.SinceTree; 74 import com.sun.source.doctree.StartElementTree; 75 import com.sun.source.doctree.SummaryTree; 76 import com.sun.source.doctree.SystemPropertyTree; 77 import com.sun.source.doctree.TextTree; 78 import com.sun.source.doctree.ThrowsTree; 79 import com.sun.source.doctree.UnknownBlockTagTree; 80 import com.sun.source.doctree.UnknownInlineTagTree; 81 import com.sun.source.doctree.UsesTree; 82 import com.sun.source.doctree.ValueTree; 83 import com.sun.source.doctree.VersionTree; 84 import com.sun.source.tree.Tree; 85 import com.sun.source.util.DocTreePath; 86 import com.sun.source.util.DocTreePathScanner; 87 import com.sun.source.util.TreePath; 88 import com.sun.tools.javac.tree.DocPretty; 89 import com.sun.tools.javac.util.Assert; 90 import com.sun.tools.javac.util.DefinedBy; 91 import com.sun.tools.javac.util.DefinedBy.Api; 92 93 import jdk.javadoc.internal.doclint.HtmlTag.AttrKind; 94 import jdk.javadoc.internal.doclint.HtmlTag.ElemKind; 95 import static jdk.javadoc.internal.doclint.Messages.Group.*; 96 97 98 /** 99 * Validate a doc comment. 100 * 101 * <p><b>This is NOT part of any supported API. 102 * If you write code that depends on this, you do so at your own 103 * risk. This code and its internal interfaces are subject to change 104 * or deletion without notice.</b></p> 105 */ 106 public class Checker extends DocTreePathScanner<Void, Void> { 107 final Env env; 108 109 Set<Element> foundParams = new HashSet<>(); 110 Set<TypeMirror> foundThrows = new HashSet<>(); 111 Map<Element, Set<String>> foundAnchors = new HashMap<>(); 112 boolean foundInheritDoc = false; 113 boolean foundReturn = false; 114 boolean hasNonWhitespaceText = false; 115 116 public enum Flag { 117 TABLE_HAS_CAPTION, 118 TABLE_IS_PRESENTATION, 119 HAS_ELEMENT, 120 HAS_HEADING, 121 HAS_INLINE_TAG, 122 HAS_TEXT, 123 REPORTED_BAD_INLINE 124 } 125 126 static class TagStackItem { 127 final DocTree tree; // typically, but not always, StartElementTree 128 final HtmlTag tag; 129 final Set<HtmlTag.Attr> attrs; 130 final Set<Flag> flags; TagStackItem(DocTree tree, HtmlTag tag)131 TagStackItem(DocTree tree, HtmlTag tag) { 132 this.tree = tree; 133 this.tag = tag; 134 attrs = EnumSet.noneOf(HtmlTag.Attr.class); 135 flags = EnumSet.noneOf(Flag.class); 136 } 137 @Override toString()138 public String toString() { 139 return String.valueOf(tag); 140 } 141 } 142 143 private final Deque<TagStackItem> tagStack; // TODO: maybe want to record starting tree as well 144 private HtmlTag currHeadingTag; 145 146 private int implicitHeadingRank; 147 private boolean inIndex; 148 private boolean inLink; 149 private boolean inSummary; 150 151 // <editor-fold defaultstate="collapsed" desc="Top level"> 152 Checker(Env env)153 Checker(Env env) { 154 this.env = Assert.checkNonNull(env); 155 tagStack = new LinkedList<>(); 156 } 157 scan(DocCommentTree tree, TreePath p)158 public Void scan(DocCommentTree tree, TreePath p) { 159 env.initTypes(); 160 env.setCurrent(p, tree); 161 162 boolean isOverridingMethod = !env.currOverriddenMethods.isEmpty(); 163 JavaFileObject fo = p.getCompilationUnit().getSourceFile(); 164 165 if (p.getLeaf().getKind() == Tree.Kind.PACKAGE) { 166 // If p points to a package, the implied declaration is the 167 // package declaration (if any) for the compilation unit. 168 // Handle this case specially, because doc comments are only 169 // expected in package-info files. 170 boolean isPkgInfo = fo.isNameCompatible("package-info", JavaFileObject.Kind.SOURCE); 171 if (tree == null) { 172 if (isPkgInfo) 173 reportMissing("dc.missing.comment"); 174 return null; 175 } else { 176 if (!isPkgInfo) 177 reportReference("dc.unexpected.comment"); 178 } 179 } else if (tree != null && fo.isNameCompatible("package", JavaFileObject.Kind.HTML)) { 180 // a package.html file with a DocCommentTree 181 if (tree.getFullBody().isEmpty()) { 182 reportMissing("dc.missing.comment"); 183 return null; 184 } 185 } else { 186 if (tree == null) { 187 if (!isSynthetic() && !isOverridingMethod) 188 reportMissing("dc.missing.comment"); 189 return null; 190 } 191 } 192 193 tagStack.clear(); 194 currHeadingTag = null; 195 196 foundParams.clear(); 197 foundThrows.clear(); 198 foundInheritDoc = false; 199 foundReturn = false; 200 hasNonWhitespaceText = false; 201 202 switch (p.getLeaf().getKind()) { 203 // the following are for declarations that have their own top-level page, 204 // and so the doc comment comes after the <h1> page title. 205 case MODULE: 206 case PACKAGE: 207 case CLASS: 208 case INTERFACE: 209 case ENUM: 210 case ANNOTATION_TYPE: 211 case RECORD: 212 implicitHeadingRank = 1; 213 break; 214 215 // this is for html files 216 // ... if it is a legacy package.html, the doc comment comes after the <h1> page title 217 // ... otherwise, (e.g. overview file and doc-files/*.html files) no additional headings are inserted 218 case COMPILATION_UNIT: 219 implicitHeadingRank = fo.isNameCompatible("package", JavaFileObject.Kind.HTML) ? 1 : 0; 220 break; 221 222 // the following are for member declarations, which appear in the page 223 // for the enclosing type, and so appear after the <h2> "Members" 224 // aggregate heading and the specific <h3> "Member signature" heading. 225 case METHOD: 226 case VARIABLE: 227 implicitHeadingRank = 3; 228 break; 229 230 default: 231 Assert.error("unexpected tree kind: " + p.getLeaf().getKind() + " " + fo); 232 } 233 234 scan(new DocTreePath(p, tree), null); 235 236 if (!isOverridingMethod) { 237 switch (env.currElement.getKind()) { 238 case METHOD: 239 case CONSTRUCTOR: { 240 ExecutableElement ee = (ExecutableElement) env.currElement; 241 checkParamsDocumented(ee.getTypeParameters()); 242 checkParamsDocumented(ee.getParameters()); 243 switch (ee.getReturnType().getKind()) { 244 case VOID: 245 case NONE: 246 break; 247 default: 248 if (!foundReturn 249 && !foundInheritDoc 250 && !env.types.isSameType(ee.getReturnType(), env.java_lang_Void)) { 251 reportMissing("dc.missing.return"); 252 } 253 } 254 checkThrowsDocumented(ee.getThrownTypes()); 255 } 256 } 257 } 258 259 return null; 260 } 261 reportMissing(String code, Object... args)262 private void reportMissing(String code, Object... args) { 263 env.messages.report(MISSING, Kind.WARNING, env.currPath.getLeaf(), code, args); 264 } 265 reportReference(String code, Object... args)266 private void reportReference(String code, Object... args) { 267 env.messages.report(REFERENCE, Kind.WARNING, env.currPath.getLeaf(), code, args); 268 } 269 270 @Override @DefinedBy(Api.COMPILER_TREE) visitDocComment(DocCommentTree tree, Void ignore)271 public Void visitDocComment(DocCommentTree tree, Void ignore) { 272 scan(tree.getFirstSentence(), ignore); 273 scan(tree.getBody(), ignore); 274 checkTagStack(); 275 276 for (DocTree blockTag : tree.getBlockTags()) { 277 tagStack.clear(); 278 scan(blockTag, ignore); 279 checkTagStack(); 280 } 281 282 return null; 283 } 284 checkTagStack()285 private void checkTagStack() { 286 for (TagStackItem tsi: tagStack) { 287 warnIfEmpty(tsi, null); 288 if (tsi.tree.getKind() == DocTree.Kind.START_ELEMENT 289 && tsi.tag.endKind == HtmlTag.EndKind.REQUIRED) { 290 StartElementTree t = (StartElementTree) tsi.tree; 291 env.messages.error(HTML, t, "dc.tag.not.closed", t.getName()); 292 } 293 } 294 } 295 // </editor-fold> 296 297 // <editor-fold defaultstate="collapsed" desc="Text and entities."> 298 299 @Override @DefinedBy(Api.COMPILER_TREE) visitText(TextTree tree, Void ignore)300 public Void visitText(TextTree tree, Void ignore) { 301 hasNonWhitespaceText = hasNonWhitespace(tree); 302 if (hasNonWhitespaceText) { 303 checkAllowsText(tree); 304 markEnclosingTag(Flag.HAS_TEXT); 305 } 306 return null; 307 } 308 309 @Override @DefinedBy(Api.COMPILER_TREE) visitEntity(EntityTree tree, Void ignore)310 public Void visitEntity(EntityTree tree, Void ignore) { 311 checkAllowsText(tree); 312 markEnclosingTag(Flag.HAS_TEXT); 313 String s = env.trees.getCharacters(tree); 314 if (s == null) { 315 env.messages.error(HTML, tree, "dc.entity.invalid", tree.getName()); 316 } 317 return null; 318 319 } 320 checkAllowsText(DocTree tree)321 void checkAllowsText(DocTree tree) { 322 TagStackItem top = tagStack.peek(); 323 if (top != null 324 && top.tree.getKind() == DocTree.Kind.START_ELEMENT 325 && !top.tag.acceptsText()) { 326 if (top.flags.add(Flag.REPORTED_BAD_INLINE)) { 327 env.messages.error(HTML, tree, "dc.text.not.allowed", 328 ((StartElementTree) top.tree).getName()); 329 } 330 } 331 } 332 333 // </editor-fold> 334 335 // <editor-fold defaultstate="collapsed" desc="HTML elements"> 336 337 @Override @DefinedBy(Api.COMPILER_TREE) visitStartElement(StartElementTree tree, Void ignore)338 public Void visitStartElement(StartElementTree tree, Void ignore) { 339 final Name treeName = tree.getName(); 340 final HtmlTag t = HtmlTag.get(treeName); 341 if (t == null) { 342 env.messages.error(HTML, tree, "dc.tag.unknown", treeName); 343 } else if (t.elemKind == ElemKind.HTML4) { 344 env.messages.error(HTML, tree, "dc.tag.not.supported.html5", treeName); 345 } else { 346 boolean done = false; 347 for (TagStackItem tsi: tagStack) { 348 if (tsi.tag.accepts(t)) { 349 while (tagStack.peek() != tsi) { 350 warnIfEmpty(tagStack.peek(), null); 351 tagStack.pop(); 352 } 353 done = true; 354 break; 355 } else if (tsi.tag.endKind != HtmlTag.EndKind.OPTIONAL) { 356 done = true; 357 break; 358 } 359 } 360 if (!done && HtmlTag.BODY.accepts(t)) { 361 while (!tagStack.isEmpty()) { 362 warnIfEmpty(tagStack.peek(), null); 363 tagStack.pop(); 364 } 365 } 366 367 markEnclosingTag(Flag.HAS_ELEMENT); 368 checkStructure(tree, t); 369 370 // tag specific checks 371 switch (t) { 372 // check for out of sequence headings, such as <h1>...</h1> <h3>...</h3> 373 case H1: case H2: case H3: case H4: case H5: case H6: 374 checkHeading(tree, t); 375 break; 376 } 377 378 if (t.flags.contains(HtmlTag.Flag.NO_NEST)) { 379 for (TagStackItem i: tagStack) { 380 if (t == i.tag) { 381 env.messages.warning(HTML, tree, "dc.tag.nested.not.allowed", treeName); 382 break; 383 } 384 } 385 } 386 387 // check for self closing tags, such as <a id="name"/> 388 if (tree.isSelfClosing() && !isSelfClosingAllowed(t)) { 389 env.messages.error(HTML, tree, "dc.tag.self.closing", treeName); 390 } 391 } 392 393 try { 394 TagStackItem parent = tagStack.peek(); 395 TagStackItem top = new TagStackItem(tree, t); 396 tagStack.push(top); 397 398 super.visitStartElement(tree, ignore); 399 400 // handle attributes that may or may not have been found in start element 401 if (t != null) { 402 switch (t) { 403 case CAPTION: 404 if (parent != null && parent.tag == HtmlTag.TABLE) 405 parent.flags.add(Flag.TABLE_HAS_CAPTION); 406 break; 407 408 case H1: case H2: case H3: case H4: case H5: case H6: 409 if (parent != null && (parent.tag == HtmlTag.SECTION || parent.tag == HtmlTag.ARTICLE)) { 410 parent.flags.add(Flag.HAS_HEADING); 411 } 412 break; 413 414 case IMG: 415 if (!top.attrs.contains(HtmlTag.Attr.ALT)) 416 env.messages.error(ACCESSIBILITY, tree, "dc.no.alt.attr.for.image"); 417 break; 418 } 419 } 420 421 return null; 422 } finally { 423 424 if (t == null || t.endKind == HtmlTag.EndKind.NONE) 425 tagStack.pop(); 426 } 427 } 428 429 // so-called "self-closing" tags are only permitted in HTML 5, for void elements 430 // https://html.spec.whatwg.org/multipage/syntax.html#start-tags isSelfClosingAllowed(HtmlTag tag)431 private boolean isSelfClosingAllowed(HtmlTag tag) { 432 return tag.endKind == HtmlTag.EndKind.NONE; 433 } 434 checkStructure(StartElementTree tree, HtmlTag t)435 private void checkStructure(StartElementTree tree, HtmlTag t) { 436 Name treeName = tree.getName(); 437 TagStackItem top = tagStack.peek(); 438 switch (t.blockType) { 439 case BLOCK: 440 if (top == null || top.tag.accepts(t)) 441 return; 442 443 switch (top.tree.getKind()) { 444 case START_ELEMENT: { 445 if (top.tag.blockType == HtmlTag.BlockType.INLINE) { 446 Name name = ((StartElementTree) top.tree).getName(); 447 env.messages.error(HTML, tree, "dc.tag.not.allowed.inline.element", 448 treeName, name); 449 return; 450 } 451 } 452 break; 453 454 case LINK: 455 case LINK_PLAIN: { 456 String name = top.tree.getKind().tagName; 457 env.messages.error(HTML, tree, "dc.tag.not.allowed.inline.tag", 458 treeName, name); 459 return; 460 } 461 } 462 break; 463 464 case INLINE: 465 if (top == null || top.tag.accepts(t)) 466 return; 467 break; 468 469 case LIST_ITEM: 470 case TABLE_ITEM: 471 if (top != null) { 472 // reset this flag so subsequent bad inline content gets reported 473 top.flags.remove(Flag.REPORTED_BAD_INLINE); 474 if (top.tag.accepts(t)) 475 return; 476 } 477 break; 478 479 case OTHER: 480 switch (t) { 481 case SCRIPT: 482 // <script> may or may not be allowed, depending on --allow-script-in-comments 483 // but we allow it here, and rely on a separate scanner to detect all uses 484 // of JavaScript, including <script> tags, and use in attributes, etc. 485 break; 486 487 default: 488 env.messages.error(HTML, tree, "dc.tag.not.allowed", treeName); 489 } 490 return; 491 } 492 493 env.messages.error(HTML, tree, "dc.tag.not.allowed.here", treeName); 494 } 495 checkHeading(StartElementTree tree, HtmlTag tag)496 private void checkHeading(StartElementTree tree, HtmlTag tag) { 497 // verify the new tag 498 if (getHeadingRank(tag) > getHeadingRank(currHeadingTag) + 1) { 499 if (currHeadingTag == null) { 500 env.messages.error(ACCESSIBILITY, tree, "dc.tag.heading.sequence.1", 501 tag, implicitHeadingRank); 502 } else { 503 env.messages.error(ACCESSIBILITY, tree, "dc.tag.heading.sequence.2", 504 tag, currHeadingTag); 505 } 506 } else if (getHeadingRank(tag) <= implicitHeadingRank) { 507 env.messages.error(ACCESSIBILITY, tree, "dc.tag.heading.sequence.3", 508 tag, implicitHeadingRank); 509 } 510 511 currHeadingTag = tag; 512 } 513 getHeadingRank(HtmlTag tag)514 private int getHeadingRank(HtmlTag tag) { 515 if (tag == null) 516 return implicitHeadingRank; 517 switch (tag) { 518 case H1: return 1; 519 case H2: return 2; 520 case H3: return 3; 521 case H4: return 4; 522 case H5: return 5; 523 case H6: return 6; 524 default: throw new IllegalArgumentException(); 525 } 526 } 527 528 @Override @DefinedBy(Api.COMPILER_TREE) visitEndElement(EndElementTree tree, Void ignore)529 public Void visitEndElement(EndElementTree tree, Void ignore) { 530 final Name treeName = tree.getName(); 531 final HtmlTag t = HtmlTag.get(treeName); 532 if (t == null) { 533 env.messages.error(HTML, tree, "dc.tag.unknown", treeName); 534 } else if (t.endKind == HtmlTag.EndKind.NONE) { 535 env.messages.error(HTML, tree, "dc.tag.end.not.permitted", treeName); 536 } else { 537 boolean done = false; 538 while (!tagStack.isEmpty()) { 539 TagStackItem top = tagStack.peek(); 540 if (t == top.tag) { 541 switch (t) { 542 case TABLE: 543 if (!top.flags.contains(Flag.TABLE_IS_PRESENTATION) 544 && !top.attrs.contains(HtmlTag.Attr.SUMMARY) 545 && !top.flags.contains(Flag.TABLE_HAS_CAPTION)) { 546 env.messages.error(ACCESSIBILITY, tree, 547 "dc.no.summary.or.caption.for.table"); 548 } 549 break; 550 551 case SECTION: 552 case ARTICLE: 553 if (!top.flags.contains(Flag.HAS_HEADING)) { 554 env.messages.error(HTML, tree, "dc.tag.requires.heading", treeName); 555 } 556 break; 557 } 558 warnIfEmpty(top, tree); 559 tagStack.pop(); 560 done = true; 561 break; 562 } else if (top.tag == null || top.tag.endKind != HtmlTag.EndKind.REQUIRED) { 563 warnIfEmpty(top, null); 564 tagStack.pop(); 565 } else { 566 boolean found = false; 567 for (TagStackItem si: tagStack) { 568 if (si.tag == t) { 569 found = true; 570 break; 571 } 572 } 573 if (found && top.tree.getKind() == DocTree.Kind.START_ELEMENT) { 574 env.messages.error(HTML, top.tree, "dc.tag.start.unmatched", 575 ((StartElementTree) top.tree).getName()); 576 tagStack.pop(); 577 } else { 578 env.messages.error(HTML, tree, "dc.tag.end.unexpected", treeName); 579 done = true; 580 break; 581 } 582 } 583 } 584 585 if (!done && tagStack.isEmpty()) { 586 env.messages.error(HTML, tree, "dc.tag.end.unexpected", treeName); 587 } 588 } 589 590 return super.visitEndElement(tree, ignore); 591 } 592 warnIfEmpty(TagStackItem tsi, DocTree endTree)593 void warnIfEmpty(TagStackItem tsi, DocTree endTree) { 594 if (tsi.tag != null && tsi.tree instanceof StartElementTree startTree) { 595 if (tsi.tag.flags.contains(HtmlTag.Flag.EXPECT_CONTENT) 596 && !tsi.flags.contains(Flag.HAS_TEXT) 597 && !tsi.flags.contains(Flag.HAS_ELEMENT) 598 && !tsi.flags.contains(Flag.HAS_INLINE_TAG) 599 && !(tsi.tag.elemKind == ElemKind.HTML4)) { 600 DocTree tree = (endTree != null) ? endTree : startTree; 601 Name treeName = startTree.getName(); 602 env.messages.warning(HTML, tree, "dc.tag.empty", treeName); 603 } 604 } 605 } 606 607 // </editor-fold> 608 609 // <editor-fold defaultstate="collapsed" desc="HTML attributes"> 610 611 @Override @DefinedBy(Api.COMPILER_TREE) @SuppressWarnings("fallthrough") visitAttribute(AttributeTree tree, Void ignore)612 public Void visitAttribute(AttributeTree tree, Void ignore) { 613 HtmlTag currTag = tagStack.peek().tag; 614 if (currTag != null && currTag.elemKind != ElemKind.HTML4) { 615 Name name = tree.getName(); 616 HtmlTag.Attr attr = currTag.getAttr(name); 617 if (attr != null) { 618 boolean first = tagStack.peek().attrs.add(attr); 619 if (!first) 620 env.messages.error(HTML, tree, "dc.attr.repeated", name); 621 } 622 // for now, doclint allows all attribute names beginning with "on" as event handler names, 623 // without checking the validity or applicability of the name 624 if (!name.toString().startsWith("on")) { 625 AttrKind k = currTag.getAttrKind(name); 626 switch (k) { 627 case OK: 628 break; 629 case OBSOLETE: 630 env.messages.warning(HTML, tree, "dc.attr.obsolete", name); 631 break; 632 case HTML4: 633 env.messages.error(HTML, tree, "dc.attr.not.supported.html5", name); 634 break; 635 case INVALID: 636 env.messages.error(HTML, tree, "dc.attr.unknown", name); 637 break; 638 } 639 } 640 641 if (attr != null) { 642 switch (attr) { 643 case ID: 644 String value = getAttrValue(tree); 645 if (value == null) { 646 env.messages.error(HTML, tree, "dc.anchor.value.missing"); 647 } else { 648 if (!validId.matcher(value).matches()) { 649 env.messages.error(HTML, tree, "dc.invalid.anchor", value); 650 } 651 if (!checkAnchor(value)) { 652 env.messages.error(HTML, tree, "dc.anchor.already.defined", value); 653 } 654 } 655 break; 656 657 case HREF: 658 if (currTag == HtmlTag.A) { 659 String v = getAttrValue(tree); 660 if (v == null || v.isEmpty()) { 661 env.messages.error(HTML, tree, "dc.attr.lacks.value"); 662 } else { 663 Matcher m = docRoot.matcher(v); 664 if (m.matches()) { 665 String rest = m.group(2); 666 if (!rest.isEmpty()) 667 checkURI(tree, rest); 668 } else { 669 checkURI(tree, v); 670 } 671 } 672 } 673 break; 674 675 case VALUE: 676 if (currTag == HtmlTag.LI) { 677 String v = getAttrValue(tree); 678 if (v == null || v.isEmpty()) { 679 env.messages.error(HTML, tree, "dc.attr.lacks.value"); 680 } else if (!validNumber.matcher(v).matches()) { 681 env.messages.error(HTML, tree, "dc.attr.not.number"); 682 } 683 } 684 break; 685 686 case BORDER: 687 if (currTag == HtmlTag.TABLE) { 688 String v = getAttrValue(tree); 689 try { 690 if (v == null || (!v.isEmpty() && Integer.parseInt(v) != 1)) { 691 env.messages.error(HTML, tree, "dc.attr.table.border.not.valid", attr); 692 } 693 } catch (NumberFormatException ex) { 694 env.messages.error(HTML, tree, "dc.attr.table.border.not.number", attr); 695 } 696 } else if (currTag == HtmlTag.IMG) { 697 String v = getAttrValue(tree); 698 try { 699 if (v == null || (!v.isEmpty() && Integer.parseInt(v) != 0)) { 700 env.messages.error(HTML, tree, "dc.attr.img.border.not.valid", attr); 701 } 702 } catch (NumberFormatException ex) { 703 env.messages.error(HTML, tree, "dc.attr.img.border.not.number", attr); 704 } 705 } 706 break; 707 708 case ROLE: 709 if (currTag == HtmlTag.TABLE) { 710 String v = getAttrValue(tree); 711 if (Objects.equals(v, "presentation")) { 712 tagStack.peek().flags.add(Flag.TABLE_IS_PRESENTATION); 713 } 714 } 715 break; 716 } 717 } 718 } 719 720 // TODO: basic check on value 721 722 return null; 723 } 724 725 checkAnchor(String name)726 private boolean checkAnchor(String name) { 727 Element e = getEnclosingPackageOrClass(env.currElement); 728 if (e == null) 729 return true; 730 Set<String> set = foundAnchors.get(e); 731 if (set == null) 732 foundAnchors.put(e, set = new HashSet<>()); 733 return set.add(name); 734 } 735 getEnclosingPackageOrClass(Element e)736 private Element getEnclosingPackageOrClass(Element e) { 737 while (e != null) { 738 switch (e.getKind()) { 739 case CLASS: 740 case ENUM: 741 case INTERFACE: 742 case PACKAGE: 743 return e; 744 default: 745 e = e.getEnclosingElement(); 746 } 747 } 748 return e; 749 } 750 751 // https://html.spec.whatwg.org/#the-id-attribute 752 private static final Pattern validId = Pattern.compile("[^\\s]+"); 753 754 private static final Pattern validNumber = Pattern.compile("-?[0-9]+"); 755 756 // pattern to remove leading {@docRoot}/? 757 private static final Pattern docRoot = Pattern.compile("(?i)(\\{@docRoot *\\}/?)?(.*)"); 758 getAttrValue(AttributeTree tree)759 private String getAttrValue(AttributeTree tree) { 760 if (tree.getValue() == null) 761 return null; 762 763 StringWriter sw = new StringWriter(); 764 try { 765 new DocPretty(sw).print(tree.getValue()); 766 } catch (IOException e) { 767 // cannot happen 768 } 769 // ignore potential use of entities for now 770 return sw.toString(); 771 } 772 checkURI(AttributeTree tree, String uri)773 private void checkURI(AttributeTree tree, String uri) { 774 // allow URIs beginning with javascript:, which would otherwise be rejected by the URI API. 775 if (uri.startsWith("javascript:")) 776 return; 777 try { 778 URI u = new URI(uri); 779 } catch (URISyntaxException e) { 780 env.messages.error(HTML, tree, "dc.invalid.uri", uri); 781 } 782 } 783 // </editor-fold> 784 785 // <editor-fold defaultstate="collapsed" desc="javadoc tags"> 786 787 @Override @DefinedBy(Api.COMPILER_TREE) visitAuthor(AuthorTree tree, Void ignore)788 public Void visitAuthor(AuthorTree tree, Void ignore) { 789 warnIfEmpty(tree, tree.getName()); 790 return super.visitAuthor(tree, ignore); 791 } 792 793 @Override @DefinedBy(Api.COMPILER_TREE) visitDocRoot(DocRootTree tree, Void ignore)794 public Void visitDocRoot(DocRootTree tree, Void ignore) { 795 markEnclosingTag(Flag.HAS_INLINE_TAG); 796 return super.visitDocRoot(tree, ignore); 797 } 798 799 @Override @DefinedBy(Api.COMPILER_TREE) visitIndex(IndexTree tree, Void ignore)800 public Void visitIndex(IndexTree tree, Void ignore) { 801 markEnclosingTag(Flag.HAS_INLINE_TAG); 802 if (inIndex) { 803 env.messages.warning(HTML, tree, "dc.tag.nested.tag", "@" + tree.getTagName()); 804 } 805 for (TagStackItem tsi : tagStack) { 806 if (tsi.tag == HtmlTag.A) { 807 env.messages.warning(HTML, tree, "dc.tag.a.within.a", 808 "{@" + tree.getTagName() + "}"); 809 break; 810 } 811 } 812 boolean prevInIndex = inIndex; 813 try { 814 inIndex = true; 815 return super.visitIndex(tree, ignore); 816 } finally { 817 inIndex = prevInIndex; 818 } 819 } 820 821 @Override @DefinedBy(Api.COMPILER_TREE) visitInheritDoc(InheritDocTree tree, Void ignore)822 public Void visitInheritDoc(InheritDocTree tree, Void ignore) { 823 markEnclosingTag(Flag.HAS_INLINE_TAG); 824 // TODO: verify on overridden method 825 foundInheritDoc = true; 826 return super.visitInheritDoc(tree, ignore); 827 } 828 829 @Override @DefinedBy(Api.COMPILER_TREE) visitLink(LinkTree tree, Void ignore)830 public Void visitLink(LinkTree tree, Void ignore) { 831 markEnclosingTag(Flag.HAS_INLINE_TAG); 832 if (inLink) { 833 env.messages.warning(HTML, tree, "dc.tag.nested.tag", "@" + tree.getTagName()); 834 } 835 boolean prevInLink = inLink; 836 // simulate inline context on tag stack 837 HtmlTag t = (tree.getKind() == DocTree.Kind.LINK) 838 ? HtmlTag.CODE : HtmlTag.SPAN; 839 tagStack.push(new TagStackItem(tree, t)); 840 try { 841 inLink = true; 842 return super.visitLink(tree, ignore); 843 } finally { 844 tagStack.pop(); 845 inLink = prevInLink; 846 } 847 } 848 849 @Override @DefinedBy(Api.COMPILER_TREE) visitLiteral(LiteralTree tree, Void ignore)850 public Void visitLiteral(LiteralTree tree, Void ignore) { 851 markEnclosingTag(Flag.HAS_INLINE_TAG); 852 if (tree.getKind() == DocTree.Kind.CODE) { 853 for (TagStackItem tsi: tagStack) { 854 if (tsi.tag == HtmlTag.CODE) { 855 env.messages.warning(HTML, tree, "dc.tag.code.within.code"); 856 break; 857 } 858 } 859 } 860 return super.visitLiteral(tree, ignore); 861 } 862 863 @Override @DefinedBy(Api.COMPILER_TREE) 864 @SuppressWarnings("fallthrough") visitParam(ParamTree tree, Void ignore)865 public Void visitParam(ParamTree tree, Void ignore) { 866 boolean typaram = tree.isTypeParameter(); 867 IdentifierTree nameTree = tree.getName(); 868 Element paramElement = nameTree != null ? env.trees.getElement(new DocTreePath(getCurrentPath(), nameTree)) : null; 869 870 if (paramElement == null) { 871 switch (env.currElement.getKind()) { 872 case CLASS: case INTERFACE: { 873 if (!typaram) { 874 env.messages.error(REFERENCE, tree, "dc.invalid.param"); 875 break; 876 } 877 } 878 case METHOD: case CONSTRUCTOR: { 879 env.messages.error(REFERENCE, nameTree, "dc.param.name.not.found"); 880 break; 881 } 882 883 default: 884 env.messages.error(REFERENCE, tree, "dc.invalid.param"); 885 break; 886 } 887 } else { 888 boolean unique = foundParams.add(paramElement); 889 890 if (!unique) { 891 env.messages.warning(REFERENCE, tree, "dc.exists.param", nameTree); 892 } 893 } 894 895 warnIfEmpty(tree, tree.getDescription()); 896 return super.visitParam(tree, ignore); 897 } 898 checkParamsDocumented(List<? extends Element> list)899 private void checkParamsDocumented(List<? extends Element> list) { 900 if (foundInheritDoc) 901 return; 902 903 for (Element e: list) { 904 if (!foundParams.contains(e)) { 905 CharSequence paramName = (e.getKind() == ElementKind.TYPE_PARAMETER) 906 ? "<" + e.getSimpleName() + ">" 907 : e.getSimpleName(); 908 reportMissing("dc.missing.param", paramName); 909 } 910 } 911 } 912 913 @Override @DefinedBy(Api.COMPILER_TREE) visitProvides(ProvidesTree tree, Void ignore)914 public Void visitProvides(ProvidesTree tree, Void ignore) { 915 Element e = env.trees.getElement(env.currPath); 916 if (e.getKind() != ElementKind.MODULE) { 917 env.messages.error(REFERENCE, tree, "dc.invalid.provides"); 918 } 919 ReferenceTree serviceType = tree.getServiceType(); 920 Element se = env.trees.getElement(new DocTreePath(getCurrentPath(), serviceType)); 921 if (se == null) { 922 env.messages.error(REFERENCE, tree, "dc.service.not.found"); 923 } 924 return super.visitProvides(tree, ignore); 925 } 926 927 @Override @DefinedBy(Api.COMPILER_TREE) visitReference(ReferenceTree tree, Void ignore)928 public Void visitReference(ReferenceTree tree, Void ignore) { 929 Element e = env.trees.getElement(getCurrentPath()); 930 if (e == null) 931 env.messages.error(REFERENCE, tree, "dc.ref.not.found"); 932 return super.visitReference(tree, ignore); 933 } 934 935 @Override @DefinedBy(Api.COMPILER_TREE) visitReturn(ReturnTree tree, Void ignore)936 public Void visitReturn(ReturnTree tree, Void ignore) { 937 if (foundReturn) { 938 env.messages.warning(REFERENCE, tree, "dc.exists.return"); 939 } 940 if (tree.isInline()) { 941 DocCommentTree dct = getCurrentPath().getDocComment(); 942 if (tree != dct.getFirstSentence().get(0)) { 943 env.messages.warning(REFERENCE, tree, "dc.return.not.first"); 944 } 945 } 946 947 Element e = env.trees.getElement(env.currPath); 948 if (e.getKind() != ElementKind.METHOD 949 || ((ExecutableElement) e).getReturnType().getKind() == TypeKind.VOID) 950 env.messages.error(REFERENCE, tree, "dc.invalid.return"); 951 foundReturn = true; 952 warnIfEmpty(tree, tree.getDescription()); 953 return super.visitReturn(tree, ignore); 954 } 955 956 @Override @DefinedBy(Api.COMPILER_TREE) visitSerialData(SerialDataTree tree, Void ignore)957 public Void visitSerialData(SerialDataTree tree, Void ignore) { 958 warnIfEmpty(tree, tree.getDescription()); 959 return super.visitSerialData(tree, ignore); 960 } 961 962 @Override @DefinedBy(Api.COMPILER_TREE) visitSerialField(SerialFieldTree tree, Void ignore)963 public Void visitSerialField(SerialFieldTree tree, Void ignore) { 964 warnIfEmpty(tree, tree.getDescription()); 965 return super.visitSerialField(tree, ignore); 966 } 967 968 @Override @DefinedBy(Api.COMPILER_TREE) visitSince(SinceTree tree, Void ignore)969 public Void visitSince(SinceTree tree, Void ignore) { 970 warnIfEmpty(tree, tree.getBody()); 971 return super.visitSince(tree, ignore); 972 } 973 974 @Override @DefinedBy(Api.COMPILER_TREE) visitSummary(SummaryTree tree, Void aVoid)975 public Void visitSummary(SummaryTree tree, Void aVoid) { 976 markEnclosingTag(Flag.HAS_INLINE_TAG); 977 if (inSummary) { 978 env.messages.warning(HTML, tree, "dc.tag.nested.tag", "@" + tree.getTagName()); 979 } 980 int idx = env.currDocComment.getFullBody().indexOf(tree); 981 // Warn if the node is preceded by non-whitespace characters, 982 // or other non-text nodes. 983 if ((idx == 1 && hasNonWhitespaceText) || idx > 1) { 984 env.messages.warning(SYNTAX, tree, "dc.invalid.summary", tree.getTagName()); 985 } 986 boolean prevInSummary = inSummary; 987 try { 988 inSummary = true; 989 return super.visitSummary(tree, aVoid); 990 } finally { 991 inSummary = prevInSummary; 992 } 993 } 994 995 @Override @DefinedBy(Api.COMPILER_TREE) visitSystemProperty(SystemPropertyTree tree, Void ignore)996 public Void visitSystemProperty(SystemPropertyTree tree, Void ignore) { 997 markEnclosingTag(Flag.HAS_INLINE_TAG); 998 for (TagStackItem tsi : tagStack) { 999 if (tsi.tag == HtmlTag.A) { 1000 env.messages.warning(HTML, tree, "dc.tag.a.within.a", 1001 "{@" + tree.getTagName() + "}"); 1002 break; 1003 } 1004 } 1005 return super.visitSystemProperty(tree, ignore); 1006 } 1007 1008 @Override @DefinedBy(Api.COMPILER_TREE) visitThrows(ThrowsTree tree, Void ignore)1009 public Void visitThrows(ThrowsTree tree, Void ignore) { 1010 ReferenceTree exName = tree.getExceptionName(); 1011 Element ex = env.trees.getElement(new DocTreePath(getCurrentPath(), exName)); 1012 if (ex == null) { 1013 env.messages.error(REFERENCE, tree, "dc.ref.not.found"); 1014 } else if (isThrowable(ex.asType())) { 1015 switch (env.currElement.getKind()) { 1016 case CONSTRUCTOR: 1017 case METHOD: 1018 if (isCheckedException(ex.asType())) { 1019 ExecutableElement ee = (ExecutableElement) env.currElement; 1020 checkThrowsDeclared(exName, ex.asType(), ee.getThrownTypes()); 1021 } 1022 break; 1023 default: 1024 env.messages.error(REFERENCE, tree, "dc.invalid.throws"); 1025 } 1026 } else { 1027 env.messages.error(REFERENCE, tree, "dc.invalid.throws"); 1028 } 1029 warnIfEmpty(tree, tree.getDescription()); 1030 return scan(tree.getDescription(), ignore); 1031 } 1032 isThrowable(TypeMirror tm)1033 private boolean isThrowable(TypeMirror tm) { 1034 switch (tm.getKind()) { 1035 case DECLARED: 1036 case TYPEVAR: 1037 return env.types.isAssignable(tm, env.java_lang_Throwable); 1038 } 1039 return false; 1040 } 1041 checkThrowsDeclared(ReferenceTree tree, TypeMirror t, List<? extends TypeMirror> list)1042 private void checkThrowsDeclared(ReferenceTree tree, TypeMirror t, List<? extends TypeMirror> list) { 1043 boolean found = false; 1044 for (TypeMirror tl : list) { 1045 if (env.types.isAssignable(t, tl)) { 1046 foundThrows.add(tl); 1047 found = true; 1048 } 1049 } 1050 if (!found) 1051 env.messages.error(REFERENCE, tree, "dc.exception.not.thrown", t); 1052 } 1053 checkThrowsDocumented(List<? extends TypeMirror> list)1054 private void checkThrowsDocumented(List<? extends TypeMirror> list) { 1055 if (foundInheritDoc) 1056 return; 1057 1058 for (TypeMirror tl: list) { 1059 if (isCheckedException(tl) && !foundThrows.contains(tl)) 1060 reportMissing("dc.missing.throws", tl); 1061 } 1062 } 1063 1064 @Override @DefinedBy(Api.COMPILER_TREE) visitUnknownBlockTag(UnknownBlockTagTree tree, Void ignore)1065 public Void visitUnknownBlockTag(UnknownBlockTagTree tree, Void ignore) { 1066 checkUnknownTag(tree, tree.getTagName()); 1067 return super.visitUnknownBlockTag(tree, ignore); 1068 } 1069 1070 @Override @DefinedBy(Api.COMPILER_TREE) visitUnknownInlineTag(UnknownInlineTagTree tree, Void ignore)1071 public Void visitUnknownInlineTag(UnknownInlineTagTree tree, Void ignore) { 1072 markEnclosingTag(Flag.HAS_INLINE_TAG); 1073 checkUnknownTag(tree, tree.getTagName()); 1074 return super.visitUnknownInlineTag(tree, ignore); 1075 } 1076 checkUnknownTag(DocTree tree, String tagName)1077 private void checkUnknownTag(DocTree tree, String tagName) { 1078 if (env.customTags != null && !env.customTags.contains(tagName)) 1079 env.messages.error(SYNTAX, tree, "dc.tag.unknown", tagName); 1080 } 1081 1082 @Override @DefinedBy(Api.COMPILER_TREE) visitUses(UsesTree tree, Void ignore)1083 public Void visitUses(UsesTree tree, Void ignore) { 1084 Element e = env.trees.getElement(env.currPath); 1085 if (e.getKind() != ElementKind.MODULE) { 1086 env.messages.error(REFERENCE, tree, "dc.invalid.uses"); 1087 } 1088 ReferenceTree serviceType = tree.getServiceType(); 1089 Element se = env.trees.getElement(new DocTreePath(getCurrentPath(), serviceType)); 1090 if (se == null) { 1091 env.messages.error(REFERENCE, tree, "dc.service.not.found"); 1092 } 1093 return super.visitUses(tree, ignore); 1094 } 1095 1096 @Override @DefinedBy(Api.COMPILER_TREE) visitValue(ValueTree tree, Void ignore)1097 public Void visitValue(ValueTree tree, Void ignore) { 1098 ReferenceTree ref = tree.getReference(); 1099 if (ref == null || ref.getSignature().isEmpty()) { 1100 if (!isConstant(env.currElement)) 1101 env.messages.error(REFERENCE, tree, "dc.value.not.allowed.here"); 1102 } else { 1103 Element e = env.trees.getElement(new DocTreePath(getCurrentPath(), ref)); 1104 if (!isConstant(e)) 1105 env.messages.error(REFERENCE, tree, "dc.value.not.a.constant"); 1106 } 1107 1108 markEnclosingTag(Flag.HAS_INLINE_TAG); 1109 return super.visitValue(tree, ignore); 1110 } 1111 isConstant(Element e)1112 private boolean isConstant(Element e) { 1113 if (e == null) 1114 return false; 1115 1116 switch (e.getKind()) { 1117 case FIELD: 1118 Object value = ((VariableElement) e).getConstantValue(); 1119 return (value != null); // can't distinguish "not a constant" from "constant is null" 1120 default: 1121 return false; 1122 } 1123 } 1124 1125 @Override @DefinedBy(Api.COMPILER_TREE) visitVersion(VersionTree tree, Void ignore)1126 public Void visitVersion(VersionTree tree, Void ignore) { 1127 warnIfEmpty(tree, tree.getBody()); 1128 return super.visitVersion(tree, ignore); 1129 } 1130 1131 @Override @DefinedBy(Api.COMPILER_TREE) visitErroneous(ErroneousTree tree, Void ignore)1132 public Void visitErroneous(ErroneousTree tree, Void ignore) { 1133 env.messages.error(SYNTAX, tree, null, tree.getDiagnostic().getMessage(null)); 1134 return null; 1135 } 1136 // </editor-fold> 1137 1138 // <editor-fold defaultstate="collapsed" desc="Utility methods"> 1139 isCheckedException(TypeMirror t)1140 private boolean isCheckedException(TypeMirror t) { 1141 return !(env.types.isAssignable(t, env.java_lang_Error) 1142 || env.types.isAssignable(t, env.java_lang_RuntimeException)); 1143 } 1144 isSynthetic()1145 private boolean isSynthetic() { 1146 switch (env.currElement.getKind()) { 1147 case CONSTRUCTOR: 1148 // A synthetic default constructor has the same pos as the 1149 // enclosing class 1150 TreePath p = env.currPath; 1151 return env.getPos(p) == env.getPos(p.getParentPath()); 1152 } 1153 return false; 1154 } 1155 markEnclosingTag(Flag flag)1156 void markEnclosingTag(Flag flag) { 1157 TagStackItem top = tagStack.peek(); 1158 if (top != null) 1159 top.flags.add(flag); 1160 } 1161 toString(TreePath p)1162 String toString(TreePath p) { 1163 StringBuilder sb = new StringBuilder("TreePath["); 1164 toString(p, sb); 1165 sb.append("]"); 1166 return sb.toString(); 1167 } 1168 toString(TreePath p, StringBuilder sb)1169 void toString(TreePath p, StringBuilder sb) { 1170 TreePath parent = p.getParentPath(); 1171 if (parent != null) { 1172 toString(parent, sb); 1173 sb.append(","); 1174 } 1175 sb.append(p.getLeaf().getKind()).append(":").append(env.getPos(p)).append(":S").append(env.getStartPos(p)); 1176 } 1177 warnIfEmpty(DocTree tree, List<? extends DocTree> list)1178 void warnIfEmpty(DocTree tree, List<? extends DocTree> list) { 1179 for (DocTree d: list) { 1180 switch (d.getKind()) { 1181 case TEXT: 1182 if (hasNonWhitespace((TextTree) d)) 1183 return; 1184 break; 1185 default: 1186 return; 1187 } 1188 } 1189 env.messages.warning(MISSING, tree, "dc.empty", tree.getKind().tagName); 1190 } 1191 hasNonWhitespace(TextTree tree)1192 boolean hasNonWhitespace(TextTree tree) { 1193 String s = tree.getBody(); 1194 for (int i = 0; i < s.length(); i++) { 1195 Character c = s.charAt(i); 1196 if (!Character.isWhitespace(s.charAt(i))) 1197 return true; 1198 } 1199 return false; 1200 } 1201 1202 // </editor-fold> 1203 1204 } 1205