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