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