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