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