Checker.java revision 2571:10fc81ac75b4
1135446Strhodes/*
2216175Sdougb * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
3135446Strhodes * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4135446Strhodes *
5186462Sdougb * This code is free software; you can redistribute it and/or modify it
6135446Strhodes * under the terms of the GNU General Public License version 2 only, as
7135446Strhodes * published by the Free Software Foundation.  Oracle designates this
8135446Strhodes * particular file as subject to the "Classpath" exception as provided
9135446Strhodes * by Oracle in the LICENSE file that accompanied this code.
10135446Strhodes *
11135446Strhodes * This code is distributed in the hope that it will be useful, but WITHOUT
12135446Strhodes * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13135446Strhodes * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14135446Strhodes * version 2 for more details (a copy is included in the LICENSE file that
15135446Strhodes * accompanied this code).
16135446Strhodes *
17135446Strhodes * You should have received a copy of the GNU General Public License version
18234010Sdougb * 2 along with this work; if not, write to the Free Software Foundation,
19135446Strhodes * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20165071Sdougb *
21165071Sdougb * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22135446Strhodes * or visit www.oracle.com if you need additional information or have any
23135446Strhodes * questions.
24135446Strhodes */
25165071Sdougb
26165071Sdougbpackage com.sun.tools.doclint;
27135446Strhodes
28135446Strhodesimport java.io.IOException;
29135446Strhodesimport java.io.StringWriter;
30135446Strhodesimport java.net.URI;
31135446Strhodesimport java.net.URISyntaxException;
32135446Strhodesimport java.util.Deque;
33135446Strhodesimport java.util.EnumSet;
34135446Strhodesimport java.util.HashMap;
35135446Strhodesimport java.util.HashSet;
36135446Strhodesimport java.util.LinkedList;
37135446Strhodesimport java.util.List;
38135446Strhodesimport java.util.Map;
39135446Strhodesimport java.util.Set;
40135446Strhodesimport java.util.regex.Matcher;
41135446Strhodesimport java.util.regex.Pattern;
42135446Strhodes
43135446Strhodesimport javax.lang.model.element.Element;
44135446Strhodesimport javax.lang.model.element.ElementKind;
45135446Strhodesimport javax.lang.model.element.ExecutableElement;
46135446Strhodesimport javax.lang.model.element.Name;
47165071Sdougbimport javax.lang.model.element.VariableElement;
48135446Strhodesimport javax.lang.model.type.TypeKind;
49135446Strhodesimport javax.lang.model.type.TypeMirror;
50135446Strhodesimport javax.tools.Diagnostic.Kind;
51135446Strhodesimport javax.tools.JavaFileObject;
52135446Strhodes
53135446Strhodesimport com.sun.source.doctree.AttributeTree;
54135446Strhodesimport com.sun.source.doctree.AuthorTree;
55135446Strhodesimport com.sun.source.doctree.DocCommentTree;
56135446Strhodesimport com.sun.source.doctree.DocRootTree;
57135446Strhodesimport com.sun.source.doctree.DocTree;
58135446Strhodesimport com.sun.source.doctree.EndElementTree;
59135446Strhodesimport com.sun.source.doctree.EntityTree;
60135446Strhodesimport com.sun.source.doctree.ErroneousTree;
61135446Strhodesimport com.sun.source.doctree.IdentifierTree;
62135446Strhodesimport com.sun.source.doctree.InheritDocTree;
63165071Sdougbimport com.sun.source.doctree.LinkTree;
64135446Strhodesimport com.sun.source.doctree.LiteralTree;
65135446Strhodesimport com.sun.source.doctree.ParamTree;
66135446Strhodesimport com.sun.source.doctree.ReferenceTree;
67135446Strhodesimport com.sun.source.doctree.ReturnTree;
68135446Strhodesimport com.sun.source.doctree.SerialDataTree;
69135446Strhodesimport com.sun.source.doctree.SerialFieldTree;
70135446Strhodesimport com.sun.source.doctree.SinceTree;
71135446Strhodesimport com.sun.source.doctree.StartElementTree;
72135446Strhodesimport com.sun.source.doctree.TextTree;
73135446Strhodesimport com.sun.source.doctree.ThrowsTree;
74135446Strhodesimport com.sun.source.doctree.UnknownBlockTagTree;
75135446Strhodesimport com.sun.source.doctree.UnknownInlineTagTree;
76135446Strhodesimport com.sun.source.doctree.ValueTree;
77135446Strhodesimport com.sun.source.doctree.VersionTree;
78135446Strhodesimport com.sun.source.tree.Tree;
79135446Strhodesimport com.sun.source.util.DocTreePath;
80135446Strhodesimport com.sun.source.util.DocTreePathScanner;
81135446Strhodesimport com.sun.source.util.TreePath;
82135446Strhodesimport com.sun.tools.doclint.HtmlTag.AttrKind;
83135446Strhodesimport com.sun.tools.javac.tree.DocPretty;
84135446Strhodesimport com.sun.tools.javac.util.StringUtils;
85135446Strhodesimport static com.sun.tools.doclint.Messages.Group.*;
86135446Strhodes
87135446Strhodes
88135446Strhodes/**
89135446Strhodes * Validate a doc comment.
90135446Strhodes *
91135446Strhodes * <p><b>This is NOT part of any supported API.
92135446Strhodes * If you write code that depends on this, you do so at your own
93135446Strhodes * risk.  This code and its internal interfaces are subject to change
94135446Strhodes * or deletion without notice.</b></p>
95135446Strhodes */
96135446Strhodespublic class Checker extends DocTreePathScanner<Void, Void> {
97135446Strhodes    final Env env;
98135446Strhodes
99135446Strhodes    Set<Element> foundParams = new HashSet<>();
100135446Strhodes    Set<TypeMirror> foundThrows = new HashSet<>();
101135446Strhodes    Map<Element, Set<String>> foundAnchors = new HashMap<>();
102135446Strhodes    boolean foundInheritDoc = false;
103135446Strhodes    boolean foundReturn = false;
104135446Strhodes
105135446Strhodes    public enum Flag {
106135446Strhodes        TABLE_HAS_CAPTION,
107135446Strhodes        HAS_ELEMENT,
108135446Strhodes        HAS_INLINE_TAG,
109135446Strhodes        HAS_TEXT,
110135446Strhodes        REPORTED_BAD_INLINE
111135446Strhodes    }
112135446Strhodes
113135446Strhodes    static class TagStackItem {
114135446Strhodes        final DocTree tree; // typically, but not always, StartElementTree
115135446Strhodes        final HtmlTag tag;
116135446Strhodes        final Set<HtmlTag.Attr> attrs;
117135446Strhodes        final Set<Flag> flags;
118135446Strhodes        TagStackItem(DocTree tree, HtmlTag tag) {
119135446Strhodes            this.tree = tree;
120135446Strhodes            this.tag = tag;
121135446Strhodes            attrs = EnumSet.noneOf(HtmlTag.Attr.class);
122135446Strhodes            flags = EnumSet.noneOf(Flag.class);
123135446Strhodes        }
124135446Strhodes        @Override
125135446Strhodes        public String toString() {
126135446Strhodes            return String.valueOf(tag);
127135446Strhodes        }
128135446Strhodes    }
129135446Strhodes
130135446Strhodes    private Deque<TagStackItem> tagStack; // TODO: maybe want to record starting tree as well
131135446Strhodes    private HtmlTag currHeaderTag;
132135446Strhodes
133135446Strhodes    private final int implicitHeaderLevel;
134135446Strhodes
135135446Strhodes    // <editor-fold defaultstate="collapsed" desc="Top level">
136135446Strhodes
137135446Strhodes    Checker(Env env) {
138135446Strhodes        env.getClass();
139135446Strhodes        this.env = env;
140135446Strhodes        tagStack = new LinkedList<>();
141135446Strhodes        implicitHeaderLevel = env.implicitHeaderLevel;
142135446Strhodes    }
143135446Strhodes
144135446Strhodes    public Void scan(DocCommentTree tree, TreePath p) {
145135446Strhodes        env.setCurrent(p, tree);
146135446Strhodes
147135446Strhodes        boolean isOverridingMethod = !env.currOverriddenMethods.isEmpty();
148135446Strhodes
149135446Strhodes        if (p.getLeaf().getKind() == Tree.Kind.PACKAGE) {
150135446Strhodes            // If p points to a package, the implied declaration is the
151135446Strhodes            // package declaration (if any) for the compilation unit.
152135446Strhodes            // Handle this case specially, because doc comments are only
153135446Strhodes            // expected in package-info files.
154135446Strhodes            JavaFileObject fo = p.getCompilationUnit().getSourceFile();
155135446Strhodes            boolean isPkgInfo = fo.isNameCompatible("package-info", JavaFileObject.Kind.SOURCE);
156135446Strhodes            if (tree == null) {
157135446Strhodes                if (isPkgInfo)
158135446Strhodes                    reportMissing("dc.missing.comment");
159135446Strhodes                return null;
160135446Strhodes            } else {
161135446Strhodes                if (!isPkgInfo)
162135446Strhodes                    reportReference("dc.unexpected.comment");
163135446Strhodes            }
164135446Strhodes        } else {
165135446Strhodes            if (tree == null) {
166135446Strhodes                if (!isSynthetic() && !isOverridingMethod)
167135446Strhodes                    reportMissing("dc.missing.comment");
168135446Strhodes                return null;
169135446Strhodes            }
170135446Strhodes        }
171135446Strhodes
172135446Strhodes        tagStack.clear();
173135446Strhodes        currHeaderTag = null;
174135446Strhodes
175135446Strhodes        foundParams.clear();
176135446Strhodes        foundThrows.clear();
177135446Strhodes        foundInheritDoc = false;
178135446Strhodes        foundReturn = false;
179135446Strhodes
180135446Strhodes        scan(new DocTreePath(p, tree), null);
181135446Strhodes
182135446Strhodes        if (!isOverridingMethod) {
183135446Strhodes            switch (env.currElement.getKind()) {
184135446Strhodes                case METHOD:
185135446Strhodes                case CONSTRUCTOR: {
186135446Strhodes                    ExecutableElement ee = (ExecutableElement) env.currElement;
187135446Strhodes                    checkParamsDocumented(ee.getTypeParameters());
188135446Strhodes                    checkParamsDocumented(ee.getParameters());
189135446Strhodes                    switch (ee.getReturnType().getKind()) {
190135446Strhodes                        case VOID:
191135446Strhodes                        case NONE:
192135446Strhodes                            break;
193135446Strhodes                        default:
194135446Strhodes                            if (!foundReturn
195135446Strhodes                                    && !foundInheritDoc
196135446Strhodes                                    && !env.types.isSameType(ee.getReturnType(), env.java_lang_Void)) {
197135446Strhodes                                reportMissing("dc.missing.return");
198135446Strhodes                            }
199135446Strhodes                    }
200135446Strhodes                    checkThrowsDocumented(ee.getThrownTypes());
201135446Strhodes                }
202135446Strhodes            }
203135446Strhodes        }
204135446Strhodes
205135446Strhodes        return null;
206135446Strhodes    }
207135446Strhodes
208135446Strhodes    private void reportMissing(String code, Object... args) {
209135446Strhodes        env.messages.report(MISSING, Kind.WARNING, env.currPath.getLeaf(), code, args);
210135446Strhodes    }
211135446Strhodes
212135446Strhodes    private void reportReference(String code, Object... args) {
213135446Strhodes        env.messages.report(REFERENCE, Kind.WARNING, env.currPath.getLeaf(), code, args);
214135446Strhodes    }
215135446Strhodes
216135446Strhodes    @Override
217135446Strhodes    public Void visitDocComment(DocCommentTree tree, Void ignore) {
218135446Strhodes        super.visitDocComment(tree, ignore);
219135446Strhodes        for (TagStackItem tsi: tagStack) {
220135446Strhodes            warnIfEmpty(tsi, null);
221135446Strhodes            if (tsi.tree.getKind() == DocTree.Kind.START_ELEMENT
222135446Strhodes                    && tsi.tag.endKind == HtmlTag.EndKind.REQUIRED) {
223135446Strhodes                StartElementTree t = (StartElementTree) tsi.tree;
224135446Strhodes                env.messages.error(HTML, t, "dc.tag.not.closed", t.getName());
225135446Strhodes            }
226135446Strhodes        }
227135446Strhodes        return null;
228135446Strhodes    }
229135446Strhodes    // </editor-fold>
230135446Strhodes
231135446Strhodes    // <editor-fold defaultstate="collapsed" desc="Text and entities.">
232135446Strhodes
233135446Strhodes    @Override
234135446Strhodes    public Void visitText(TextTree tree, Void ignore) {
235135446Strhodes        if (hasNonWhitespace(tree)) {
236135446Strhodes            checkAllowsText(tree);
237135446Strhodes            markEnclosingTag(Flag.HAS_TEXT);
238135446Strhodes        }
239135446Strhodes        return null;
240135446Strhodes    }
241135446Strhodes
242135446Strhodes    @Override
243135446Strhodes    public Void visitEntity(EntityTree tree, Void ignore) {
244135446Strhodes        checkAllowsText(tree);
245135446Strhodes        markEnclosingTag(Flag.HAS_TEXT);
246135446Strhodes        String name = tree.getName().toString();
247135446Strhodes        if (name.startsWith("#")) {
248135446Strhodes            int v = StringUtils.toLowerCase(name).startsWith("#x")
249186462Sdougb                    ? Integer.parseInt(name.substring(2), 16)
250186462Sdougb                    : Integer.parseInt(name.substring(1), 10);
251186462Sdougb            if (!Entity.isValid(v)) {
252186462Sdougb                env.messages.error(HTML, tree, "dc.entity.invalid", name);
253186462Sdougb            }
254186462Sdougb        } else if (!Entity.isValid(name)) {
255186462Sdougb            env.messages.error(HTML, tree, "dc.entity.invalid", name);
256186462Sdougb        }
257186462Sdougb        return null;
258186462Sdougb    }
259186462Sdougb
260186462Sdougb    void checkAllowsText(DocTree tree) {
261186462Sdougb        TagStackItem top = tagStack.peek();
262186462Sdougb        if (top != null
263186462Sdougb                && top.tree.getKind() == DocTree.Kind.START_ELEMENT
264186462Sdougb                && !top.tag.acceptsText()) {
265186462Sdougb            if (top.flags.add(Flag.REPORTED_BAD_INLINE)) {
266186462Sdougb                env.messages.error(HTML, tree, "dc.text.not.allowed",
267135446Strhodes                        ((StartElementTree) top.tree).getName());
268135446Strhodes            }
269135446Strhodes        }
270135446Strhodes    }
271135446Strhodes
272135446Strhodes    // </editor-fold>
273135446Strhodes
274135446Strhodes    // <editor-fold defaultstate="collapsed" desc="HTML elements">
275186462Sdougb
276186462Sdougb    @Override
277186462Sdougb    public Void visitStartElement(StartElementTree tree, Void ignore) {
278186462Sdougb        final Name treeName = tree.getName();
279186462Sdougb        final HtmlTag t = HtmlTag.get(treeName);
280186462Sdougb        if (t == null) {
281186462Sdougb            env.messages.error(HTML, tree, "dc.tag.unknown", treeName);
282186462Sdougb        } else {
283186462Sdougb            boolean done = false;
284186462Sdougb            for (TagStackItem tsi: tagStack) {
285186462Sdougb                if (tsi.tag.accepts(t)) {
286186462Sdougb                    while (tagStack.peek() != tsi) {
287186462Sdougb                        warnIfEmpty(tagStack.peek(), null);
288186462Sdougb                        tagStack.pop();
289186462Sdougb                    }
290186462Sdougb                    done = true;
291186462Sdougb                    break;
292186462Sdougb                } else if (tsi.tag.endKind != HtmlTag.EndKind.OPTIONAL) {
293186462Sdougb                    done = true;
294186462Sdougb                    break;
295186462Sdougb                }
296186462Sdougb            }
297186462Sdougb            if (!done && HtmlTag.BODY.accepts(t)) {
298135446Strhodes                while (!tagStack.isEmpty()) {
299135446Strhodes                    warnIfEmpty(tagStack.peek(), null);
300135446Strhodes                    tagStack.pop();
301135446Strhodes                }
302135446Strhodes            }
303135446Strhodes
304135446Strhodes            markEnclosingTag(Flag.HAS_ELEMENT);
305135446Strhodes            checkStructure(tree, t);
306186462Sdougb
307186462Sdougb            // tag specific checks
308186462Sdougb            switch (t) {
309186462Sdougb                // check for out of sequence headers, such as <h1>...</h1>  <h3>...</h3>
310186462Sdougb                case H1: case H2: case H3: case H4: case H5: case H6:
311186462Sdougb                    checkHeader(tree, t);
312186462Sdougb                    break;
313186462Sdougb            }
314186462Sdougb
315186462Sdougb            if (t.flags.contains(HtmlTag.Flag.NO_NEST)) {
316186462Sdougb                for (TagStackItem i: tagStack) {
317186462Sdougb                    if (t == i.tag) {
318186462Sdougb                        env.messages.warning(HTML, tree, "dc.tag.nested.not.allowed", treeName);
319186462Sdougb                        break;
320186462Sdougb                    }
321186462Sdougb                }
322186462Sdougb            }
323186462Sdougb        }
324135446Strhodes
325135446Strhodes        // check for self closing tags, such as <a id="name"/>
326135446Strhodes        if (tree.isSelfClosing()) {
327135446Strhodes            env.messages.error(HTML, tree, "dc.tag.self.closing", treeName);
328135446Strhodes        }
329135446Strhodes
330135446Strhodes        try {
331135446Strhodes            TagStackItem parent = tagStack.peek();
332135446Strhodes            TagStackItem top = new TagStackItem(tree, t);
333135446Strhodes            tagStack.push(top);
334135446Strhodes
335135446Strhodes            super.visitStartElement(tree, ignore);
336135446Strhodes
337186462Sdougb            // handle attributes that may or may not have been found in start element
338186462Sdougb            if (t != null) {
339186462Sdougb                switch (t) {
340186462Sdougb                    case CAPTION:
341186462Sdougb                        if (parent != null && parent.tag == HtmlTag.TABLE)
342186462Sdougb                            parent.flags.add(Flag.TABLE_HAS_CAPTION);
343186462Sdougb                        break;
344186462Sdougb
345186462Sdougb                    case IMG:
346135446Strhodes                        if (!top.attrs.contains(HtmlTag.Attr.ALT))
347135446Strhodes                            env.messages.error(ACCESSIBILITY, tree, "dc.no.alt.attr.for.image");
348135446Strhodes                        break;
349135446Strhodes                }
350135446Strhodes            }
351135446Strhodes
352135446Strhodes            return null;
353135446Strhodes        } finally {
354135446Strhodes
355135446Strhodes            if (t == null || t.endKind == HtmlTag.EndKind.NONE)
356135446Strhodes                tagStack.pop();
357135446Strhodes        }
358135446Strhodes    }
359186462Sdougb
360186462Sdougb    private void checkStructure(StartElementTree tree, HtmlTag t) {
361186462Sdougb        Name treeName = tree.getName();
362186462Sdougb        TagStackItem top = tagStack.peek();
363186462Sdougb        switch (t.blockType) {
364186462Sdougb            case BLOCK:
365186462Sdougb                if (top == null || top.tag.accepts(t))
366186462Sdougb                    return;
367186462Sdougb
368135446Strhodes                switch (top.tree.getKind()) {
369135446Strhodes                    case START_ELEMENT: {
370135446Strhodes                        if (top.tag.blockType == HtmlTag.BlockType.INLINE) {
371135446Strhodes                            Name name = ((StartElementTree) top.tree).getName();
372135446Strhodes                            env.messages.error(HTML, tree, "dc.tag.not.allowed.inline.element",
373135446Strhodes                                    treeName, name);
374135446Strhodes                            return;
375135446Strhodes                        }
376135446Strhodes                    }
377135446Strhodes                    break;
378135446Strhodes
379135446Strhodes                    case LINK:
380135446Strhodes                    case LINK_PLAIN: {
381135446Strhodes                        String name = top.tree.getKind().tagName;
382135446Strhodes                        env.messages.error(HTML, tree, "dc.tag.not.allowed.inline.tag",
383135446Strhodes                                treeName, name);
384135446Strhodes                        return;
385135446Strhodes                    }
386135446Strhodes                }
387135446Strhodes                break;
388135446Strhodes
389135446Strhodes            case INLINE:
390135446Strhodes                if (top == null || top.tag.accepts(t))
391135446Strhodes                    return;
392135446Strhodes                break;
393135446Strhodes
394135446Strhodes            case LIST_ITEM:
395135446Strhodes            case TABLE_ITEM:
396135446Strhodes                if (top != null) {
397135446Strhodes                    // reset this flag so subsequent bad inline content gets reported
398135446Strhodes                    top.flags.remove(Flag.REPORTED_BAD_INLINE);
399135446Strhodes                    if (top.tag.accepts(t))
400135446Strhodes                        return;
401135446Strhodes                }
402135446Strhodes                break;
403135446Strhodes
404135446Strhodes            case OTHER:
405135446Strhodes                env.messages.error(HTML, tree, "dc.tag.not.allowed", treeName);
406135446Strhodes                return;
407135446Strhodes        }
408135446Strhodes
409135446Strhodes        env.messages.error(HTML, tree, "dc.tag.not.allowed.here", treeName);
410135446Strhodes    }
411135446Strhodes
412135446Strhodes    private void checkHeader(StartElementTree tree, HtmlTag tag) {
413135446Strhodes        // verify the new tag
414135446Strhodes        if (getHeaderLevel(tag) > getHeaderLevel(currHeaderTag) + 1) {
415135446Strhodes            if (currHeaderTag == null) {
416135446Strhodes                env.messages.error(ACCESSIBILITY, tree, "dc.tag.header.sequence.1", tag);
417135446Strhodes            } else {
418135446Strhodes                env.messages.error(ACCESSIBILITY, tree, "dc.tag.header.sequence.2",
419135446Strhodes                    tag, currHeaderTag);
420135446Strhodes            }
421135446Strhodes        }
422135446Strhodes
423135446Strhodes        currHeaderTag = tag;
424135446Strhodes    }
425135446Strhodes
426135446Strhodes    private int getHeaderLevel(HtmlTag tag) {
427135446Strhodes        if (tag == null)
428135446Strhodes            return implicitHeaderLevel;
429135446Strhodes        switch (tag) {
430135446Strhodes            case H1: return 1;
431135446Strhodes            case H2: return 2;
432135446Strhodes            case H3: return 3;
433135446Strhodes            case H4: return 4;
434135446Strhodes            case H5: return 5;
435135446Strhodes            case H6: return 6;
436135446Strhodes            default: throw new IllegalArgumentException();
437135446Strhodes        }
438135446Strhodes    }
439135446Strhodes
440135446Strhodes    @Override
441135446Strhodes    public Void visitEndElement(EndElementTree tree, Void ignore) {
442135446Strhodes        final Name treeName = tree.getName();
443135446Strhodes        final HtmlTag t = HtmlTag.get(treeName);
444135446Strhodes        if (t == null) {
445135446Strhodes            env.messages.error(HTML, tree, "dc.tag.unknown", treeName);
446135446Strhodes        } else if (t.endKind == HtmlTag.EndKind.NONE) {
447135446Strhodes            env.messages.error(HTML, tree, "dc.tag.end.not.permitted", treeName);
448135446Strhodes        } else {
449135446Strhodes            boolean done = false;
450135446Strhodes            while (!tagStack.isEmpty()) {
451135446Strhodes                TagStackItem top = tagStack.peek();
452135446Strhodes                if (t == top.tag) {
453135446Strhodes                    switch (t) {
454135446Strhodes                        case TABLE:
455135446Strhodes                            if (!top.attrs.contains(HtmlTag.Attr.SUMMARY)
456135446Strhodes                                    && !top.flags.contains(Flag.TABLE_HAS_CAPTION)) {
457135446Strhodes                                env.messages.error(ACCESSIBILITY, tree,
458135446Strhodes                                        "dc.no.summary.or.caption.for.table");
459135446Strhodes                            }
460135446Strhodes                    }
461135446Strhodes                    warnIfEmpty(top, tree);
462135446Strhodes                    tagStack.pop();
463135446Strhodes                    done = true;
464135446Strhodes                    break;
465135446Strhodes                } else if (top.tag == null || top.tag.endKind != HtmlTag.EndKind.REQUIRED) {
466135446Strhodes                    tagStack.pop();
467135446Strhodes                } else {
468135446Strhodes                    boolean found = false;
469135446Strhodes                    for (TagStackItem si: tagStack) {
470135446Strhodes                        if (si.tag == t) {
471216175Sdougb                            found = true;
472135446Strhodes                            break;
473135446Strhodes                        }
474135446Strhodes                    }
475135446Strhodes                    if (found && top.tree.getKind() == DocTree.Kind.START_ELEMENT) {
476135446Strhodes                        env.messages.error(HTML, top.tree, "dc.tag.start.unmatched",
477135446Strhodes                                ((StartElementTree) top.tree).getName());
478135446Strhodes                        tagStack.pop();
479135446Strhodes                    } else {
480135446Strhodes                        env.messages.error(HTML, tree, "dc.tag.end.unexpected", treeName);
481135446Strhodes                        done = true;
482135446Strhodes                        break;
483135446Strhodes                    }
484135446Strhodes                }
485135446Strhodes            }
486135446Strhodes
487135446Strhodes            if (!done && tagStack.isEmpty()) {
488135446Strhodes                env.messages.error(HTML, tree, "dc.tag.end.unexpected", treeName);
489135446Strhodes            }
490135446Strhodes        }
491135446Strhodes
492135446Strhodes        return super.visitEndElement(tree, ignore);
493135446Strhodes    }
494135446Strhodes
495135446Strhodes    void warnIfEmpty(TagStackItem tsi, DocTree endTree) {
496135446Strhodes        if (tsi.tag != null && tsi.tree instanceof StartElementTree) {
497135446Strhodes            if (tsi.tag.flags.contains(HtmlTag.Flag.EXPECT_CONTENT)
498135446Strhodes                    && !tsi.flags.contains(Flag.HAS_TEXT)
499135446Strhodes                    && !tsi.flags.contains(Flag.HAS_ELEMENT)
500135446Strhodes                    && !tsi.flags.contains(Flag.HAS_INLINE_TAG)) {
501135446Strhodes                DocTree tree = (endTree != null) ? endTree : tsi.tree;
502135446Strhodes                Name treeName = ((StartElementTree) tsi.tree).getName();
503135446Strhodes                env.messages.warning(HTML, tree, "dc.tag.empty", treeName);
504135446Strhodes            }
505135446Strhodes        }
506135446Strhodes    }
507135446Strhodes
508135446Strhodes    // </editor-fold>
509135446Strhodes
510135446Strhodes    // <editor-fold defaultstate="collapsed" desc="HTML attributes">
511135446Strhodes
512135446Strhodes    @Override @SuppressWarnings("fallthrough")
513135446Strhodes    public Void visitAttribute(AttributeTree tree, Void ignore) {
514135446Strhodes        HtmlTag currTag = tagStack.peek().tag;
515135446Strhodes        if (currTag != null) {
516135446Strhodes            Name name = tree.getName();
517135446Strhodes            HtmlTag.Attr attr = currTag.getAttr(name);
518135446Strhodes            if (attr != null) {
519135446Strhodes                boolean first = tagStack.peek().attrs.add(attr);
520135446Strhodes                if (!first)
521135446Strhodes                    env.messages.error(HTML, tree, "dc.attr.repeated", name);
522135446Strhodes            }
523135446Strhodes            AttrKind k = currTag.getAttrKind(name);
524135446Strhodes            switch (k) {
525135446Strhodes                case OK:
526135446Strhodes                    break;
527135446Strhodes
528135446Strhodes                case INVALID:
529135446Strhodes                    env.messages.error(HTML, tree, "dc.attr.unknown", name);
530135446Strhodes                    break;
531135446Strhodes
532135446Strhodes                case OBSOLETE:
533135446Strhodes                    env.messages.warning(ACCESSIBILITY, tree, "dc.attr.obsolete", name);
534135446Strhodes                    break;
535135446Strhodes
536135446Strhodes                case USE_CSS:
537135446Strhodes                    env.messages.warning(ACCESSIBILITY, tree, "dc.attr.obsolete.use.css", name);
538135446Strhodes                    break;
539135446Strhodes            }
540135446Strhodes
541135446Strhodes            if (attr != null) {
542135446Strhodes                switch (attr) {
543135446Strhodes                    case NAME:
544135446Strhodes                        if (currTag != HtmlTag.A) {
545135446Strhodes                            break;
546135446Strhodes                        }
547135446Strhodes                        // fallthrough
548135446Strhodes                    case ID:
549135446Strhodes                        String value = getAttrValue(tree);
550135446Strhodes                        if (value == null) {
551135446Strhodes                            env.messages.error(HTML, tree, "dc.anchor.value.missing");
552135446Strhodes                        } else {
553135446Strhodes                            if (!validName.matcher(value).matches()) {
554135446Strhodes                                env.messages.error(HTML, tree, "dc.invalid.anchor", value);
555135446Strhodes                            }
556135446Strhodes                            if (!checkAnchor(value)) {
557135446Strhodes                                env.messages.error(HTML, tree, "dc.anchor.already.defined", value);
558135446Strhodes                            }
559135446Strhodes                        }
560135446Strhodes                        break;
561135446Strhodes
562135446Strhodes                    case HREF:
563135446Strhodes                        if (currTag == HtmlTag.A) {
564135446Strhodes                            String v = getAttrValue(tree);
565135446Strhodes                            if (v == null || v.isEmpty()) {
566135446Strhodes                                env.messages.error(HTML, tree, "dc.attr.lacks.value");
567135446Strhodes                            } else {
568135446Strhodes                                Matcher m = docRoot.matcher(v);
569135446Strhodes                                if (m.matches()) {
570135446Strhodes                                    String rest = m.group(2);
571135446Strhodes                                    if (!rest.isEmpty())
572135446Strhodes                                        checkURI(tree, rest);
573135446Strhodes                                } else {
574135446Strhodes                                    checkURI(tree, v);
575135446Strhodes                                }
576135446Strhodes                            }
577135446Strhodes                        }
578135446Strhodes                        break;
579135446Strhodes
580135446Strhodes                    case VALUE:
581135446Strhodes                        if (currTag == HtmlTag.LI) {
582135446Strhodes                            String v = getAttrValue(tree);
583135446Strhodes                            if (v == null || v.isEmpty()) {
584135446Strhodes                                env.messages.error(HTML, tree, "dc.attr.lacks.value");
585135446Strhodes                            } else if (!validNumber.matcher(v).matches()) {
586135446Strhodes                                env.messages.error(HTML, tree, "dc.attr.not.number");
587135446Strhodes                            }
588135446Strhodes                        }
589135446Strhodes                        break;
590135446Strhodes                }
591135446Strhodes            }
592135446Strhodes        }
593135446Strhodes
594135446Strhodes        // TODO: basic check on value
595135446Strhodes
596135446Strhodes        return super.visitAttribute(tree, ignore);
597135446Strhodes    }
598135446Strhodes
599135446Strhodes    private boolean checkAnchor(String name) {
600135446Strhodes        Element e = getEnclosingPackageOrClass(env.currElement);
601135446Strhodes        if (e == null)
602135446Strhodes            return true;
603135446Strhodes        Set<String> set = foundAnchors.get(e);
604135446Strhodes        if (set == null)
605135446Strhodes            foundAnchors.put(e, set = new HashSet<>());
606135446Strhodes        return set.add(name);
607135446Strhodes    }
608135446Strhodes
609135446Strhodes    private Element getEnclosingPackageOrClass(Element e) {
610135446Strhodes        while (e != null) {
611135446Strhodes            switch (e.getKind()) {
612135446Strhodes                case CLASS:
613135446Strhodes                case ENUM:
614135446Strhodes                case INTERFACE:
615135446Strhodes                case PACKAGE:
616135446Strhodes                    return e;
617135446Strhodes                default:
618135446Strhodes                    e = e.getEnclosingElement();
619135446Strhodes            }
620135446Strhodes        }
621135446Strhodes        return e;
622135446Strhodes    }
623135446Strhodes
624135446Strhodes    // http://www.w3.org/TR/html401/types.html#type-name
625    private static final Pattern validName = Pattern.compile("[A-Za-z][A-Za-z0-9-_:.]*");
626
627    private static final Pattern validNumber = Pattern.compile("-?[0-9]+");
628
629    // pattern to remove leading {@docRoot}/?
630    private static final Pattern docRoot = Pattern.compile("(?i)(\\{@docRoot *\\}/?)?(.*)");
631
632    private String getAttrValue(AttributeTree tree) {
633        if (tree.getValue() == null)
634            return null;
635
636        StringWriter sw = new StringWriter();
637        try {
638            new DocPretty(sw).print(tree.getValue());
639        } catch (IOException e) {
640            // cannot happen
641        }
642        // ignore potential use of entities for now
643        return sw.toString();
644    }
645
646    private void checkURI(AttributeTree tree, String uri) {
647        try {
648            URI u = new URI(uri);
649        } catch (URISyntaxException e) {
650            env.messages.error(HTML, tree, "dc.invalid.uri", uri);
651        }
652    }
653    // </editor-fold>
654
655    // <editor-fold defaultstate="collapsed" desc="javadoc tags">
656
657    @Override
658    public Void visitAuthor(AuthorTree tree, Void ignore) {
659        warnIfEmpty(tree, tree.getName());
660        return super.visitAuthor(tree, ignore);
661    }
662
663    @Override
664    public Void visitDocRoot(DocRootTree tree, Void ignore) {
665        markEnclosingTag(Flag.HAS_INLINE_TAG);
666        return super.visitDocRoot(tree, ignore);
667    }
668
669    @Override
670    public Void visitInheritDoc(InheritDocTree tree, Void ignore) {
671        markEnclosingTag(Flag.HAS_INLINE_TAG);
672        // TODO: verify on overridden method
673        foundInheritDoc = true;
674        return super.visitInheritDoc(tree, ignore);
675    }
676
677    @Override
678    public Void visitLink(LinkTree tree, Void ignore) {
679        markEnclosingTag(Flag.HAS_INLINE_TAG);
680        // simulate inline context on tag stack
681        HtmlTag t = (tree.getKind() == DocTree.Kind.LINK)
682                ? HtmlTag.CODE : HtmlTag.SPAN;
683        tagStack.push(new TagStackItem(tree, t));
684        try {
685            return super.visitLink(tree, ignore);
686        } finally {
687            tagStack.pop();
688        }
689    }
690
691    @Override
692    public Void visitLiteral(LiteralTree tree, Void ignore) {
693        markEnclosingTag(Flag.HAS_INLINE_TAG);
694        if (tree.getKind() == DocTree.Kind.CODE) {
695            for (TagStackItem tsi: tagStack) {
696                if (tsi.tag == HtmlTag.CODE) {
697                    env.messages.warning(HTML, tree, "dc.tag.code.within.code");
698                    break;
699                }
700            }
701        }
702        return super.visitLiteral(tree, ignore);
703    }
704
705    @Override
706    @SuppressWarnings("fallthrough")
707    public Void visitParam(ParamTree tree, Void ignore) {
708        boolean typaram = tree.isTypeParameter();
709        IdentifierTree nameTree = tree.getName();
710        Element paramElement = nameTree != null ? env.trees.getElement(new DocTreePath(getCurrentPath(), nameTree)) : null;
711
712        if (paramElement == null) {
713            switch (env.currElement.getKind()) {
714                case CLASS: case INTERFACE: {
715                    if (!typaram) {
716                        env.messages.error(REFERENCE, tree, "dc.invalid.param");
717                        break;
718                    }
719                }
720                case METHOD: case CONSTRUCTOR: {
721                    env.messages.error(REFERENCE, nameTree, "dc.param.name.not.found");
722                    break;
723                }
724
725                default:
726                    env.messages.error(REFERENCE, tree, "dc.invalid.param");
727                    break;
728            }
729        } else {
730            foundParams.add(paramElement);
731        }
732
733        warnIfEmpty(tree, tree.getDescription());
734        return super.visitParam(tree, ignore);
735    }
736
737    private void checkParamsDocumented(List<? extends Element> list) {
738        if (foundInheritDoc)
739            return;
740
741        for (Element e: list) {
742            if (!foundParams.contains(e)) {
743                CharSequence paramName = (e.getKind() == ElementKind.TYPE_PARAMETER)
744                        ? "<" + e.getSimpleName() + ">"
745                        : e.getSimpleName();
746                reportMissing("dc.missing.param", paramName);
747            }
748        }
749    }
750
751    @Override
752    public Void visitReference(ReferenceTree tree, Void ignore) {
753        String sig = tree.getSignature();
754        if (sig.contains("<") || sig.contains(">"))
755            env.messages.error(REFERENCE, tree, "dc.type.arg.not.allowed");
756
757        Element e = env.trees.getElement(getCurrentPath());
758        if (e == null)
759            env.messages.error(REFERENCE, tree, "dc.ref.not.found");
760        return super.visitReference(tree, ignore);
761    }
762
763    @Override
764    public Void visitReturn(ReturnTree tree, Void ignore) {
765        Element e = env.trees.getElement(env.currPath);
766        if (e.getKind() != ElementKind.METHOD
767                || ((ExecutableElement) e).getReturnType().getKind() == TypeKind.VOID)
768            env.messages.error(REFERENCE, tree, "dc.invalid.return");
769        foundReturn = true;
770        warnIfEmpty(tree, tree.getDescription());
771        return super.visitReturn(tree, ignore);
772    }
773
774    @Override
775    public Void visitSerialData(SerialDataTree tree, Void ignore) {
776        warnIfEmpty(tree, tree.getDescription());
777        return super.visitSerialData(tree, ignore);
778    }
779
780    @Override
781    public Void visitSerialField(SerialFieldTree tree, Void ignore) {
782        warnIfEmpty(tree, tree.getDescription());
783        return super.visitSerialField(tree, ignore);
784    }
785
786    @Override
787    public Void visitSince(SinceTree tree, Void ignore) {
788        warnIfEmpty(tree, tree.getBody());
789        return super.visitSince(tree, ignore);
790    }
791
792    @Override
793    public Void visitThrows(ThrowsTree tree, Void ignore) {
794        ReferenceTree exName = tree.getExceptionName();
795        Element ex = env.trees.getElement(new DocTreePath(getCurrentPath(), exName));
796        if (ex == null) {
797            env.messages.error(REFERENCE, tree, "dc.ref.not.found");
798        } else if (isThrowable(ex.asType())) {
799            switch (env.currElement.getKind()) {
800                case CONSTRUCTOR:
801                case METHOD:
802                    if (isCheckedException(ex.asType())) {
803                        ExecutableElement ee = (ExecutableElement) env.currElement;
804                        checkThrowsDeclared(exName, ex.asType(), ee.getThrownTypes());
805                    }
806                    break;
807                default:
808                    env.messages.error(REFERENCE, tree, "dc.invalid.throws");
809            }
810        } else {
811            env.messages.error(REFERENCE, tree, "dc.invalid.throws");
812        }
813        warnIfEmpty(tree, tree.getDescription());
814        return scan(tree.getDescription(), ignore);
815    }
816
817    private boolean isThrowable(TypeMirror tm) {
818        switch (tm.getKind()) {
819            case DECLARED:
820            case TYPEVAR:
821                return env.types.isAssignable(tm, env.java_lang_Throwable);
822        }
823        return false;
824    }
825
826    private void checkThrowsDeclared(ReferenceTree tree, TypeMirror t, List<? extends TypeMirror> list) {
827        boolean found = false;
828        for (TypeMirror tl : list) {
829            if (env.types.isAssignable(t, tl)) {
830                foundThrows.add(tl);
831                found = true;
832            }
833        }
834        if (!found)
835            env.messages.error(REFERENCE, tree, "dc.exception.not.thrown", t);
836    }
837
838    private void checkThrowsDocumented(List<? extends TypeMirror> list) {
839        if (foundInheritDoc)
840            return;
841
842        for (TypeMirror tl: list) {
843            if (isCheckedException(tl) && !foundThrows.contains(tl))
844                reportMissing("dc.missing.throws", tl);
845        }
846    }
847
848    @Override
849    public Void visitUnknownBlockTag(UnknownBlockTagTree tree, Void ignore) {
850        checkUnknownTag(tree, tree.getTagName());
851        return super.visitUnknownBlockTag(tree, ignore);
852    }
853
854    @Override
855    public Void visitUnknownInlineTag(UnknownInlineTagTree tree, Void ignore) {
856        checkUnknownTag(tree, tree.getTagName());
857        return super.visitUnknownInlineTag(tree, ignore);
858    }
859
860    private void checkUnknownTag(DocTree tree, String tagName) {
861        if (env.customTags != null && !env.customTags.contains(tagName))
862            env.messages.error(SYNTAX, tree, "dc.tag.unknown", tagName);
863    }
864
865    @Override
866    public Void visitValue(ValueTree tree, Void ignore) {
867        ReferenceTree ref = tree.getReference();
868        if (ref == null || ref.getSignature().isEmpty()) {
869            if (!isConstant(env.currElement))
870                env.messages.error(REFERENCE, tree, "dc.value.not.allowed.here");
871        } else {
872            Element e = env.trees.getElement(new DocTreePath(getCurrentPath(), ref));
873            if (!isConstant(e))
874                env.messages.error(REFERENCE, tree, "dc.value.not.a.constant");
875        }
876
877        markEnclosingTag(Flag.HAS_INLINE_TAG);
878        return super.visitValue(tree, ignore);
879    }
880
881    private boolean isConstant(Element e) {
882        if (e == null)
883            return false;
884
885        switch (e.getKind()) {
886            case FIELD:
887                Object value = ((VariableElement) e).getConstantValue();
888                return (value != null); // can't distinguish "not a constant" from "constant is null"
889            default:
890                return false;
891        }
892    }
893
894    @Override
895    public Void visitVersion(VersionTree tree, Void ignore) {
896        warnIfEmpty(tree, tree.getBody());
897        return super.visitVersion(tree, ignore);
898    }
899
900    @Override
901    public Void visitErroneous(ErroneousTree tree, Void ignore) {
902        env.messages.error(SYNTAX, tree, null, tree.getDiagnostic().getMessage(null));
903        return null;
904    }
905    // </editor-fold>
906
907    // <editor-fold defaultstate="collapsed" desc="Utility methods">
908
909    private boolean isCheckedException(TypeMirror t) {
910        return !(env.types.isAssignable(t, env.java_lang_Error)
911                || env.types.isAssignable(t, env.java_lang_RuntimeException));
912    }
913
914    private boolean isSynthetic() {
915        switch (env.currElement.getKind()) {
916            case CONSTRUCTOR:
917                // A synthetic default constructor has the same pos as the
918                // enclosing class
919                TreePath p = env.currPath;
920                return env.getPos(p) == env.getPos(p.getParentPath());
921        }
922        return false;
923    }
924
925    void markEnclosingTag(Flag flag) {
926        TagStackItem top = tagStack.peek();
927        if (top != null)
928            top.flags.add(flag);
929    }
930
931    String toString(TreePath p) {
932        StringBuilder sb = new StringBuilder("TreePath[");
933        toString(p, sb);
934        sb.append("]");
935        return sb.toString();
936    }
937
938    void toString(TreePath p, StringBuilder sb) {
939        TreePath parent = p.getParentPath();
940        if (parent != null) {
941            toString(parent, sb);
942            sb.append(",");
943        }
944       sb.append(p.getLeaf().getKind()).append(":").append(env.getPos(p)).append(":S").append(env.getStartPos(p));
945    }
946
947    void warnIfEmpty(DocTree tree, List<? extends DocTree> list) {
948        for (DocTree d: list) {
949            switch (d.getKind()) {
950                case TEXT:
951                    if (hasNonWhitespace((TextTree) d))
952                        return;
953                    break;
954                default:
955                    return;
956            }
957        }
958        env.messages.warning(SYNTAX, tree, "dc.empty", tree.getKind().tagName);
959    }
960
961    boolean hasNonWhitespace(TextTree tree) {
962        String s = tree.getBody();
963        for (int i = 0; i < s.length(); i++) {
964            if (!Character.isWhitespace(s.charAt(i)))
965                return true;
966        }
967        return false;
968    }
969
970    // </editor-fold>
971
972}
973