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