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