001/* 002 * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package org.jdrupes.mdoclet.internal.doclint; 027 028import static org.jdrupes.mdoclet.internal.doclint.Messages.Group.*; 029 030import java.io.IOException; 031import java.io.StringWriter; 032import java.net.URI; 033import java.net.URISyntaxException; 034import java.util.Deque; 035import java.util.EnumSet; 036import java.util.HashMap; 037import java.util.HashSet; 038import java.util.Iterator; 039import java.util.LinkedList; 040import java.util.List; 041import java.util.Map; 042import java.util.Objects; 043import java.util.Set; 044import java.util.regex.Matcher; 045import java.util.regex.Pattern; 046 047import javax.lang.model.SourceVersion; 048import javax.lang.model.element.Element; 049import javax.lang.model.element.ElementKind; 050import javax.lang.model.element.ExecutableElement; 051import javax.lang.model.element.Name; 052import javax.lang.model.element.NestingKind; 053import javax.lang.model.element.RecordComponentElement; 054import javax.lang.model.element.TypeElement; 055import javax.lang.model.element.VariableElement; 056import javax.lang.model.type.TypeKind; 057import javax.lang.model.type.TypeMirror; 058import javax.lang.model.util.Elements; 059import javax.tools.Diagnostic.Kind; 060 061import org.jdrupes.mdoclet.internal.doclint.HtmlTag.AttrKind; 062import org.jdrupes.mdoclet.internal.doclint.HtmlTag.ElemKind; 063 064import javax.tools.JavaFileObject; 065 066import com.sun.source.doctree.AttributeTree; 067import com.sun.source.doctree.AuthorTree; 068import com.sun.source.doctree.DocCommentTree; 069import com.sun.source.doctree.DocRootTree; 070import com.sun.source.doctree.DocTree; 071import com.sun.source.doctree.EndElementTree; 072import com.sun.source.doctree.EntityTree; 073import com.sun.source.doctree.ErroneousTree; 074import com.sun.source.doctree.EscapeTree; 075import com.sun.source.doctree.IdentifierTree; 076import com.sun.source.doctree.IndexTree; 077import com.sun.source.doctree.InheritDocTree; 078import com.sun.source.doctree.LinkTree; 079import com.sun.source.doctree.LiteralTree; 080import com.sun.source.doctree.ParamTree; 081import com.sun.source.doctree.ProvidesTree; 082import com.sun.source.doctree.ReferenceTree; 083import com.sun.source.doctree.ReturnTree; 084import com.sun.source.doctree.SerialDataTree; 085import com.sun.source.doctree.SerialFieldTree; 086import com.sun.source.doctree.SinceTree; 087import com.sun.source.doctree.StartElementTree; 088import com.sun.source.doctree.SummaryTree; 089import com.sun.source.doctree.SystemPropertyTree; 090import com.sun.source.doctree.TextTree; 091import com.sun.source.doctree.ThrowsTree; 092import com.sun.source.doctree.UnknownBlockTagTree; 093import com.sun.source.doctree.UnknownInlineTagTree; 094import com.sun.source.doctree.UsesTree; 095import com.sun.source.doctree.ValueTree; 096import com.sun.source.doctree.VersionTree; 097import com.sun.source.tree.Tree; 098import com.sun.source.util.DocTreePath; 099import com.sun.source.util.DocTreePathScanner; 100import com.sun.source.util.TreePath; 101import com.sun.tools.javac.tree.DocPretty; 102import com.sun.tools.javac.util.Assert; 103import com.sun.tools.javac.util.DefinedBy; 104import com.sun.tools.javac.util.DefinedBy.Api; 105 106/** 107 * Validate a doc comment. 108 */ 109public class Checker extends DocTreePathScanner<Void, Void> { 110 final Env env; 111 112 Set<Element> foundParams = new HashSet<>(); 113 Set<TypeMirror> foundThrows = new HashSet<>(); 114 Map<Element, Set<String>> foundAnchors = new HashMap<>(); 115 boolean foundInheritDoc = false; 116 boolean foundReturn = false; 117 boolean hasNonWhitespaceText = false; 118 119 public enum Flag { 120 TABLE_HAS_CAPTION, 121 TABLE_IS_PRESENTATION, 122 HAS_ELEMENT, 123 HAS_HEADING, 124 HAS_INLINE_TAG, 125 HAS_TEXT, 126 REPORTED_BAD_INLINE 127 } 128 129 static class TagStackItem { 130 final DocTree tree; // typically, but not always, StartElementTree 131 final HtmlTag tag; 132 final Set<HtmlTag.Attr> attrs; 133 final Set<Flag> flags; 134 135 TagStackItem(DocTree tree, HtmlTag tag) { 136 this.tree = tree; 137 this.tag = tag; 138 attrs = EnumSet.noneOf(HtmlTag.Attr.class); 139 flags = EnumSet.noneOf(Flag.class); 140 } 141 142 @Override 143 public String toString() { 144 return String.valueOf(tag); 145 } 146 } 147 148 private final Deque<TagStackItem> tagStack; // TODO: maybe want to record 149 // starting tree as well 150 private HtmlTag currHeadingTag; 151 152 private int implicitHeadingRank; 153 private boolean inIndex; 154 private boolean inLink; 155 private boolean inSummary; 156 157 // <editor-fold defaultstate="collapsed" desc="Top level"> 158 159 Checker(Env env) { 160 this.env = Assert.checkNonNull(env); 161 tagStack = new LinkedList<>(); 162 } 163 164 public Void scan(DocCommentTree tree, TreePath p) { 165 env.initTypes(); 166 env.setCurrent(p, tree); 167 168 boolean isOverridingMethod = !env.currOverriddenMethods.isEmpty(); 169 JavaFileObject fo = p.getCompilationUnit().getSourceFile(); 170 171 if (p.getLeaf().getKind() == Tree.Kind.PACKAGE) { 172 // If p points to a package, the implied declaration is the 173 // package declaration (if any) for the compilation unit. 174 // Handle this case specially, because doc comments are only 175 // expected in package-info files. 176 boolean isPkgInfo = fo.isNameCompatible("package-info", 177 JavaFileObject.Kind.SOURCE); 178 if (tree == null) { 179 if (isPkgInfo) 180 reportMissing("dc.missing.comment"); 181 return null; 182 } else { 183 if (!isPkgInfo) 184 reportReference("dc.unexpected.comment"); 185 } 186 } else if (tree != null 187 && fo.isNameCompatible("package", JavaFileObject.Kind.HTML)) { 188 // a package.html file with a DocCommentTree 189 if (tree.getFullBody().isEmpty()) { 190 reportMissing("dc.missing.comment"); 191 return null; 192 } 193 } else { 194 if (tree == null) { 195 if (isDefaultConstructor()) { 196 if (isNormalClass(p.getParentPath())) { 197 reportMissing("dc.default.constructor"); 198 } 199 } else if (!isOverridingMethod && !isSynthetic() 200 && !isAnonymous() && !isRecordComponentOrField()) { 201 reportMissing("dc.missing.comment"); 202 } 203 return null; 204 } else if (tree.getFirstSentence().isEmpty() && !isOverridingMethod 205 && !pseudoElement(p)) { 206 if (tree.getBlockTags().isEmpty()) { 207 reportMissing("dc.empty.comment"); 208 return null; 209 } else { 210 // Don't report an empty description if the comment contains 211 // @deprecated, 212 // because javadoc will use the content of that tag in 213 // summary tables. 214 if (tree.getBlockTags().stream().allMatch( 215 t -> t.getKind() != DocTree.Kind.DEPRECATED)) { 216 env.messages.report(MISSING, Kind.WARNING, tree, 217 "dc.empty.main.description"); 218 } 219 } 220 } 221 } 222 223 tagStack.clear(); 224 currHeadingTag = null; 225 226 foundParams.clear(); 227 foundThrows.clear(); 228 foundInheritDoc = false; 229 foundReturn = false; 230 hasNonWhitespaceText = false; 231 232 implicitHeadingRank = switch (p.getLeaf().getKind()) { 233 // the following are for declarations that have their own top-level 234 // page, 235 // and so the doc comment comes after the <h1> page title. 236 case MODULE, PACKAGE, CLASS, INTERFACE, ENUM, ANNOTATION_TYPE, RECORD -> 1; 237 238 // this is for html files 239 // ... if it is a legacy package.html, the doc comment comes after the 240 // <h1> page title 241 // ... otherwise, (e.g. overview file and doc-files/**/*.html files) no 242 // additional headings are inserted 243 case COMPILATION_UNIT -> fo.isNameCompatible("package", 244 JavaFileObject.Kind.HTML) ? 1 : 0; 245 246 // the following are for member declarations, which appear in the page 247 // for the enclosing type, and so appear after the <h2> "Members" 248 // aggregate heading and the specific <h3> "Member signature" heading. 249 case METHOD, VARIABLE -> 3; 250 251 default -> throw new AssertionError( 252 "unexpected tree kind: " + p.getLeaf().getKind() + " " + fo); 253 }; 254 255 scan(new DocTreePath(p, tree), null); 256 257 // the following checks are made after the scan, which will record 258 // @param tags 259 if (isDeclaredType()) { 260 TypeElement te = (TypeElement) env.currElement; 261 checkParamsDocumented(te.getTypeParameters()); 262 checkParamsDocumented(te.getRecordComponents()); 263 } else if (isExecutable()) { 264 if (!isOverridingMethod) { 265 ExecutableElement ee = (ExecutableElement) env.currElement; 266 if (!isCanonicalRecordConstructor(ee)) { 267 checkParamsDocumented(ee.getTypeParameters()); 268 checkParamsDocumented(ee.getParameters()); 269 } 270 switch (ee.getReturnType().getKind()) { 271 case VOID, NONE -> { 272 } 273 default -> { 274 if (!foundReturn 275 && !foundInheritDoc 276 && !env.types.isSameType(ee.getReturnType(), 277 env.java_lang_Void)) { 278 reportMissing("dc.missing.return"); 279 } 280 } 281 } 282 checkThrowsDocumented(ee.getThrownTypes()); 283 } 284 } 285 286 return null; 287 } 288 289 private boolean isCanonicalRecordConstructor(ExecutableElement ee) { 290 TypeElement te = (TypeElement) ee.getEnclosingElement(); 291 if (te.getKind() != ElementKind.RECORD) { 292 return false; 293 } 294 List<? extends RecordComponentElement> stateComps 295 = te.getRecordComponents(); 296 List<? extends VariableElement> params = ee.getParameters(); 297 if (stateComps.size() != params.size()) { 298 return false; 299 } 300 301 Iterator<? extends RecordComponentElement> stateIter 302 = stateComps.iterator(); 303 Iterator<? extends VariableElement> paramIter = params.iterator(); 304 while (paramIter.hasNext() && stateIter.hasNext()) { 305 VariableElement param = paramIter.next(); 306 RecordComponentElement comp = stateIter.next(); 307 if (!Objects.equals(param.getSimpleName(), comp.getSimpleName()) 308 || !env.types.isSameType(param.asType(), comp.asType())) { 309 return false; 310 } 311 } 312 313 return true; 314 } 315 316 // Checks if the passed tree path corresponds to an entity, such as 317 // the overview file and doc-files/**/*.html files. 318 private boolean pseudoElement(TreePath p) { 319 return p.getLeaf().getKind() == Tree.Kind.COMPILATION_UNIT 320 && p.getCompilationUnit().getSourceFile() 321 .getKind() == JavaFileObject.Kind.HTML; 322 } 323 324 private void reportMissing(String code, Object... args) { 325 env.messages.report(MISSING, Kind.WARNING, env.currPath.getLeaf(), code, 326 args); 327 } 328 329 private void reportReference(String code, Object... args) { 330 env.messages.report(REFERENCE, Kind.WARNING, env.currPath.getLeaf(), 331 code, args); 332 } 333 334 @Override 335 @DefinedBy(Api.COMPILER_TREE) 336 public Void visitDocComment(DocCommentTree tree, Void ignore) { 337 scan(tree.getFirstSentence(), ignore); 338 scan(tree.getBody(), ignore); 339 checkTagStack(); 340 341 for (DocTree blockTag : tree.getBlockTags()) { 342 tagStack.clear(); 343 scan(blockTag, ignore); 344 checkTagStack(); 345 } 346 347 return null; 348 } 349 350 private void checkTagStack() { 351 for (TagStackItem tsi : tagStack) { 352 warnIfEmpty(tsi, null); 353 if (tsi.tree.getKind() == DocTree.Kind.START_ELEMENT 354 && tsi.tag.endKind == HtmlTag.EndKind.REQUIRED) { 355 StartElementTree t = (StartElementTree) tsi.tree; 356 env.messages.error(HTML, t, "dc.tag.not.closed", t.getName()); 357 } 358 } 359 } 360 // </editor-fold> 361 362 // <editor-fold defaultstate="collapsed" desc="Text and entities."> 363 364 @Override 365 @DefinedBy(Api.COMPILER_TREE) 366 public Void visitText(TextTree tree, Void ignore) { 367 hasNonWhitespaceText = hasNonWhitespace(tree); 368 if (hasNonWhitespaceText) { 369 checkAllowsText(tree); 370 markEnclosingTag(Flag.HAS_TEXT); 371 } 372 return null; 373 } 374 375 @Override 376 @DefinedBy(Api.COMPILER_TREE) 377 public Void visitEntity(EntityTree tree, Void ignore) { 378 hasNonWhitespaceText = true; 379 checkAllowsText(tree); 380 markEnclosingTag(Flag.HAS_TEXT); 381 String s = env.trees.getCharacters(tree); 382 if (s == null) { 383 env.messages.error(HTML, tree, "dc.entity.invalid", tree.getName()); 384 } 385 return null; 386 387 } 388 389 @Override 390 @DefinedBy(Api.COMPILER_TREE) 391 public Void visitEscape(EscapeTree tree, Void ignore) { 392 hasNonWhitespaceText = true; 393 checkAllowsText(tree); 394 markEnclosingTag(Flag.HAS_TEXT); 395 return null; 396 } 397 398 void checkAllowsText(DocTree tree) { 399 TagStackItem top = tagStack.peek(); 400 if (top != null 401 && top.tree.getKind() == DocTree.Kind.START_ELEMENT 402 && !top.tag.acceptsText()) { 403 if (top.flags.add(Flag.REPORTED_BAD_INLINE)) { 404 env.messages.error(HTML, tree, "dc.text.not.allowed", 405 ((StartElementTree) top.tree).getName()); 406 } 407 } 408 } 409 410 // </editor-fold> 411 412 // <editor-fold defaultstate="collapsed" desc="HTML elements"> 413 414 @Override 415 @DefinedBy(Api.COMPILER_TREE) 416 public Void visitStartElement(StartElementTree tree, Void ignore) { 417 final Name treeName = tree.getName(); 418 final HtmlTag t = HtmlTag.get(treeName); 419 if (t == null) { 420 env.messages.error(HTML, tree, "dc.tag.unknown", treeName); 421 } else if (t.elemKind == ElemKind.HTML4) { 422 env.messages.error(HTML, tree, "dc.tag.not.supported.html5", 423 treeName); 424 } else { 425 boolean done = false; 426 for (TagStackItem tsi : tagStack) { 427 if (tsi.tag.accepts(t)) { 428 while (tagStack.peek() != tsi) { 429 warnIfEmpty(tagStack.peek(), null); 430 tagStack.pop(); 431 } 432 done = true; 433 break; 434 } else if (tsi.tag.endKind != HtmlTag.EndKind.OPTIONAL) { 435 done = true; 436 break; 437 } 438 } 439 if (!done && HtmlTag.BODY.accepts(t)) { 440 while (!tagStack.isEmpty()) { 441 warnIfEmpty(tagStack.peek(), null); 442 tagStack.pop(); 443 } 444 } 445 446 markEnclosingTag(Flag.HAS_ELEMENT); 447 checkStructure(tree, t); 448 449 // tag specific checks 450 switch (t) { 451 // check for out of sequence headings, such as <h1>...</h1> 452 // <h3>...</h3> 453 case H1, H2, H3, H4, H5, H6 -> checkHeading(tree, t); 454 } 455 456 if (t.flags.contains(HtmlTag.Flag.NO_NEST)) { 457 for (TagStackItem i : tagStack) { 458 if (t == i.tag) { 459 env.messages.warning(HTML, tree, 460 "dc.tag.nested.not.allowed", treeName); 461 break; 462 } 463 } 464 } 465 466 // check for self-closing tags, such as <a id="name"/> 467 if (tree.isSelfClosing() && !isSelfClosingAllowed(t)) { 468 env.messages.error(HTML, tree, "dc.tag.self.closing", treeName); 469 } 470 } 471 472 try { 473 TagStackItem parent = tagStack.peek(); 474 TagStackItem top = new TagStackItem(tree, t); 475 tagStack.push(top); 476 477 super.visitStartElement(tree, ignore); 478 479 // handle attributes that may or may not have been found in start 480 // element 481 if (t != null) { 482 switch (t) { 483 case CAPTION -> { 484 if (parent != null && parent.tag == HtmlTag.TABLE) 485 parent.flags.add(Flag.TABLE_HAS_CAPTION); 486 } 487 488 case H1, H2, H3, H4, H5, H6 -> { 489 if (parent != null && (parent.tag == HtmlTag.SECTION 490 || parent.tag == HtmlTag.ARTICLE)) { 491 parent.flags.add(Flag.HAS_HEADING); 492 } 493 } 494 495 case IMG -> { 496 if (!top.attrs.contains(HtmlTag.Attr.ALT)) 497 env.messages.error(ACCESSIBILITY, tree, 498 "dc.no.alt.attr.for.image"); 499 } 500 } 501 } 502 503 return null; 504 } finally { 505 506 if (t == null || t.endKind == HtmlTag.EndKind.NONE) 507 tagStack.pop(); 508 } 509 } 510 511 // so-called "self-closing" tags are only permitted in HTML 5, for void 512 // elements 513 // https://html.spec.whatwg.org/multipage/syntax.html#start-tags 514 private boolean isSelfClosingAllowed(HtmlTag tag) { 515 return tag.endKind == HtmlTag.EndKind.NONE; 516 } 517 518 private void checkStructure(StartElementTree tree, HtmlTag t) { 519 Name treeName = tree.getName(); 520 TagStackItem top = tagStack.peek(); 521 switch (t.blockType) { 522 case BLOCK -> { 523 if (top == null || top.tag.accepts(t)) 524 return; 525 526 switch (top.tree.getKind()) { 527 case START_ELEMENT -> { 528 if (top.tag.blockType == HtmlTag.BlockType.INLINE) { 529 Name name = ((StartElementTree) top.tree).getName(); 530 // Links may use block display style so issue warning 531 // instead of error 532 if ("a".equalsIgnoreCase(name.toString())) { 533 env.messages.warning(HTML, tree, 534 "dc.tag.not.allowed.element.default.style", 535 treeName, name); 536 } else { 537 env.messages.error(HTML, tree, 538 "dc.tag.not.allowed.inline.element", 539 treeName, name); 540 } 541 return; 542 } 543 } 544 545 case LINK, LINK_PLAIN -> { 546 String name = top.tree.getKind().tagName; 547 env.messages.warning(HTML, tree, 548 "dc.tag.not.allowed.tag.default.style", 549 treeName, name); 550 return; 551 } 552 } 553 } 554 555 case INLINE -> { 556 if (top == null || top.tag.accepts(t)) 557 return; 558 } 559 560 case LIST_ITEM, TABLE_ITEM -> { 561 if (top != null) { 562 // reset this flag so subsequent bad inline content gets 563 // reported 564 top.flags.remove(Flag.REPORTED_BAD_INLINE); 565 if (top.tag.accepts(t)) 566 return; 567 } 568 } 569 570 case OTHER -> { 571 switch (t) { 572 case SCRIPT -> { 573 // <script> may or may not be allowed, depending on 574 // --allow-script-in-comments, 575 // but we allow it here, and rely on a separate scanner to 576 // detect all uses 577 // of JavaScript, including <script> tags, and use in 578 // attributes, etc. 579 } 580 581 default -> env.messages.error(HTML, tree, "dc.tag.not.allowed", 582 treeName); 583 } 584 return; 585 } 586 } 587 588 env.messages.error(HTML, tree, "dc.tag.not.allowed.here", treeName); 589 } 590 591 private void checkHeading(StartElementTree tree, HtmlTag tag) { 592 // verify the new tag 593 if (getHeadingRank(tag) > getHeadingRank(currHeadingTag) + 1) { 594 if (currHeadingTag == null) { 595 env.messages.error(ACCESSIBILITY, tree, 596 "dc.tag.heading.sequence.1", 597 tag, implicitHeadingRank); 598 } else { 599 env.messages.error(ACCESSIBILITY, tree, 600 "dc.tag.heading.sequence.2", 601 tag, currHeadingTag); 602 } 603 } else if (getHeadingRank(tag) <= implicitHeadingRank) { 604 env.messages.error(ACCESSIBILITY, tree, "dc.tag.heading.sequence.3", 605 tag, implicitHeadingRank); 606 } 607 608 currHeadingTag = tag; 609 } 610 611 private int getHeadingRank(HtmlTag tag) { 612 return (tag == null) 613 ? implicitHeadingRank 614 : switch (tag) { 615 case H1 -> 1; 616 case H2 -> 2; 617 case H3 -> 3; 618 case H4 -> 4; 619 case H5 -> 5; 620 case H6 -> 6; 621 default -> throw new IllegalArgumentException(); 622 }; 623 } 624 625 @Override 626 @DefinedBy(Api.COMPILER_TREE) 627 public Void visitEndElement(EndElementTree tree, Void ignore) { 628 final Name treeName = tree.getName(); 629 final HtmlTag t = HtmlTag.get(treeName); 630 if (t == null) { 631 env.messages.error(HTML, tree, "dc.tag.unknown", treeName); 632 } else if (t.endKind == HtmlTag.EndKind.NONE) { 633 env.messages.error(HTML, tree, "dc.tag.end.not.permitted", 634 treeName); 635 } else { 636 boolean done = false; 637 while (!tagStack.isEmpty()) { 638 TagStackItem top = tagStack.peek(); 639 if (t == top.tag) { 640 switch (t) { 641 case TABLE -> { 642 if (!top.flags.contains(Flag.TABLE_IS_PRESENTATION) 643 && !top.attrs.contains(HtmlTag.Attr.SUMMARY) 644 && !top.flags.contains(Flag.TABLE_HAS_CAPTION)) { 645 env.messages.error(ACCESSIBILITY, tree, 646 "dc.no.summary.or.caption.for.table"); 647 } 648 } 649 650 case SECTION, ARTICLE -> { 651 if (!top.flags.contains(Flag.HAS_HEADING)) { 652 env.messages.error(HTML, tree, 653 "dc.tag.requires.heading", treeName); 654 } 655 } 656 } 657 warnIfEmpty(top, tree); 658 tagStack.pop(); 659 done = true; 660 break; 661 } else if (top.tag == null 662 || top.tag.endKind != HtmlTag.EndKind.REQUIRED) { 663 warnIfEmpty(top, null); 664 tagStack.pop(); 665 } else { 666 boolean found = false; 667 for (TagStackItem si : tagStack) { 668 if (si.tag == t) { 669 found = true; 670 break; 671 } 672 } 673 if (found 674 && top.tree.getKind() == DocTree.Kind.START_ELEMENT) { 675 env.messages.error(HTML, top.tree, 676 "dc.tag.start.unmatched", 677 ((StartElementTree) top.tree).getName()); 678 tagStack.pop(); 679 } else { 680 env.messages.error(HTML, tree, "dc.tag.end.unexpected", 681 treeName); 682 done = true; 683 break; 684 } 685 } 686 } 687 688 if (!done && tagStack.isEmpty()) { 689 env.messages.error(HTML, tree, "dc.tag.end.unexpected", 690 treeName); 691 } 692 } 693 694 return super.visitEndElement(tree, ignore); 695 } 696 697 void warnIfEmpty(TagStackItem tsi, DocTree endTree) { 698 if (tsi.tag != null && tsi.tree instanceof StartElementTree startTree) { 699 if (tsi.tag.flags.contains(HtmlTag.Flag.EXPECT_CONTENT) 700 && !tsi.flags.contains(Flag.HAS_TEXT) 701 && !tsi.flags.contains(Flag.HAS_ELEMENT) 702 && !tsi.flags.contains(Flag.HAS_INLINE_TAG) 703 && !(tsi.tag.elemKind == ElemKind.HTML4)) { 704 DocTree tree = (endTree != null) ? endTree : startTree; 705 Name treeName = startTree.getName(); 706 env.messages.warning(HTML, tree, "dc.tag.empty", treeName); 707 } 708 } 709 } 710 711 // </editor-fold> 712 713 // <editor-fold defaultstate="collapsed" desc="HTML attributes"> 714 715 @Override 716 @DefinedBy(Api.COMPILER_TREE) 717 @SuppressWarnings("fallthrough") 718 public Void visitAttribute(AttributeTree tree, Void ignore) { 719 // for now, ensure we're in an HTML StartElementTree; 720 // in time, we might check uses of attributes in other tree nodes 721 if (getParentKind() != DocTree.Kind.START_ELEMENT) { 722 return null; 723 } 724 725 HtmlTag currTag = tagStack.peek().tag; 726 if (currTag != null && currTag.elemKind != ElemKind.HTML4) { 727 Name name = tree.getName(); 728 HtmlTag.Attr attr = currTag.getAttr(name); 729 if (attr != null) { 730 boolean first = tagStack.peek().attrs.add(attr); 731 if (!first) 732 env.messages.error(HTML, tree, "dc.attr.repeated", name); 733 } 734 // for now, doclint allows all attribute names beginning with "on" 735 // as event handler names, 736 // without checking the validity or applicability of the name 737 if (!name.toString().startsWith("on")) { 738 AttrKind k = currTag.getAttrKind(name); 739 switch (k) { 740 case OK -> { 741 } 742 743 case OBSOLETE -> env.messages.warning(HTML, tree, 744 "dc.attr.obsolete", name); 745 746 case HTML4 -> env.messages.error(HTML, tree, 747 "dc.attr.not.supported.html5", name); 748 749 case INVALID -> env.messages.error(HTML, tree, 750 "dc.attr.unknown", name); 751 } 752 } 753 754 if (attr != null) { 755 switch (attr) { 756 case ID -> { 757 String value = getAttrValue(tree); 758 if (value == null) { 759 env.messages.error(HTML, tree, 760 "dc.anchor.value.missing"); 761 } else { 762 if (!validId.matcher(value).matches()) { 763 env.messages.error(HTML, tree, "dc.invalid.anchor", 764 value); 765 } 766 if (!checkAnchor(value)) { 767 env.messages.error(HTML, tree, 768 "dc.anchor.already.defined", value); 769 } 770 } 771 } 772 773 case HREF -> { 774 if (currTag == HtmlTag.A) { 775 String v = getAttrValue(tree); 776 if (v == null || v.isEmpty()) { 777 env.messages.error(HTML, tree, 778 "dc.attr.lacks.value"); 779 } else { 780 Matcher m = docRoot.matcher(v); 781 if (m.matches()) { 782 String rest = m.group(2); 783 if (!rest.isEmpty()) 784 checkURI(tree, rest); 785 } else { 786 checkURI(tree, v); 787 } 788 } 789 } 790 } 791 792 case VALUE -> { 793 if (currTag == HtmlTag.LI) { 794 String v = getAttrValue(tree); 795 if (v == null || v.isEmpty()) { 796 env.messages.error(HTML, tree, 797 "dc.attr.lacks.value"); 798 } else if (!validNumber.matcher(v).matches()) { 799 env.messages.error(HTML, tree, 800 "dc.attr.not.number"); 801 } 802 } 803 } 804 805 case BORDER -> { 806 if (currTag == HtmlTag.TABLE) { 807 String v = getAttrValue(tree); 808 try { 809 if (v == null 810 || (!v.isEmpty() && Integer.parseInt(v) != 1)) { 811 env.messages.error(HTML, tree, 812 "dc.attr.table.border.not.valid", attr); 813 } 814 } catch (NumberFormatException ex) { 815 env.messages.error(HTML, tree, 816 "dc.attr.table.border.not.number", attr); 817 } 818 } else if (currTag == HtmlTag.IMG) { 819 String v = getAttrValue(tree); 820 try { 821 if (v == null 822 || (!v.isEmpty() && Integer.parseInt(v) != 0)) { 823 env.messages.error(HTML, tree, 824 "dc.attr.img.border.not.valid", attr); 825 } 826 } catch (NumberFormatException ex) { 827 env.messages.error(HTML, tree, 828 "dc.attr.img.border.not.number", attr); 829 } 830 } 831 } 832 833 case ROLE -> { 834 if (currTag == HtmlTag.TABLE) { 835 String v = getAttrValue(tree); 836 if (Objects.equals(v, "presentation")) { 837 tagStack.peek().flags 838 .add(Flag.TABLE_IS_PRESENTATION); 839 } 840 } 841 } 842 } 843 } 844 } 845 846 // TODO: basic check on value 847 848 return null; 849 } 850 851 private boolean checkAnchor(String name) { 852 var e = getEnclosingPackageOrClass(env.currElement); 853 return e == null 854 || foundAnchors.computeIfAbsent(e, k -> new HashSet<>()).add(name); 855 } 856 857 private Element getEnclosingPackageOrClass(Element e) { 858 while (e != null) { 859 if (e.getKind().isDeclaredType() 860 || e.getKind() == ElementKind.PACKAGE) { 861 return e; 862 } 863 864 e = e.getEnclosingElement(); 865 } 866 return e; 867 } 868 869 // https://html.spec.whatwg.org/#the-id-attribute 870 private static final Pattern validId = Pattern.compile("[^\\s]+"); 871 872 private static final Pattern validNumber = Pattern.compile("-?[0-9]+"); 873 874 // pattern to remove leading {@docRoot}/? 875 private static final Pattern docRoot 876 = Pattern.compile("(?i)(\\{@docRoot *}/?)?(.*)"); 877 878 private String getAttrValue(AttributeTree tree) { 879 if (tree.getValue() == null) 880 return null; 881 882 StringWriter sw = new StringWriter(); 883 try { 884 new DocPretty(sw).print(tree.getValue()); 885 } catch (IOException e) { 886 // cannot happen 887 } 888 // ignore potential use of entities for now 889 return sw.toString(); 890 } 891 892 private void checkURI(AttributeTree tree, String uri) { 893 // allow URIs beginning with javascript:, which would otherwise be 894 // rejected by the URI API. 895 if (uri.startsWith("javascript:")) 896 return; 897 try { 898 new URI(uri); 899 } catch (URISyntaxException e) { 900 env.messages.error(HTML, tree, "dc.invalid.uri", uri); 901 } 902 } 903 // </editor-fold> 904 905 // <editor-fold defaultstate="collapsed" desc="javadoc tags"> 906 907 @Override 908 @DefinedBy(Api.COMPILER_TREE) 909 public Void visitAuthor(AuthorTree tree, Void ignore) { 910 warnIfEmpty(tree, tree.getName()); 911 return super.visitAuthor(tree, ignore); 912 } 913 914 @Override 915 @DefinedBy(Api.COMPILER_TREE) 916 public Void visitDocRoot(DocRootTree tree, Void ignore) { 917 markEnclosingTag(Flag.HAS_INLINE_TAG); 918 return super.visitDocRoot(tree, ignore); 919 } 920 921 @Override 922 @DefinedBy(Api.COMPILER_TREE) 923 public Void visitIndex(IndexTree tree, Void ignore) { 924 markEnclosingTag(Flag.HAS_INLINE_TAG); 925 if (inIndex) { 926 env.messages.warning(HTML, tree, "dc.tag.nested.tag", 927 "@" + tree.getTagName()); 928 } 929 for (TagStackItem tsi : tagStack) { 930 if (tsi.tag == HtmlTag.A) { 931 env.messages.warning(HTML, tree, "dc.tag.a.within.a", 932 "{@" + tree.getTagName() + "}"); 933 break; 934 } 935 } 936 boolean prevInIndex = inIndex; 937 try { 938 inIndex = true; 939 return super.visitIndex(tree, ignore); 940 } finally { 941 inIndex = prevInIndex; 942 } 943 } 944 945 @Override 946 @DefinedBy(Api.COMPILER_TREE) 947 public Void visitInheritDoc(InheritDocTree tree, Void ignore) { 948 markEnclosingTag(Flag.HAS_INLINE_TAG); 949 // TODO: verify on overridden method 950 foundInheritDoc = true; 951 return super.visitInheritDoc(tree, ignore); 952 } 953 954 @Override 955 @DefinedBy(Api.COMPILER_TREE) 956 public Void visitLink(LinkTree tree, Void ignore) { 957 markEnclosingTag(Flag.HAS_INLINE_TAG); 958 if (inLink) { 959 env.messages.warning(HTML, tree, "dc.tag.nested.tag", 960 "@" + tree.getTagName()); 961 } 962 boolean prevInLink = inLink; 963 // simulate inline context on tag stack 964 HtmlTag t = (tree.getKind() == DocTree.Kind.LINK) 965 ? HtmlTag.CODE 966 : HtmlTag.SPAN; 967 tagStack.push(new TagStackItem(tree, t)); 968 try { 969 inLink = true; 970 return super.visitLink(tree, ignore); 971 } finally { 972 tagStack.pop(); 973 inLink = prevInLink; 974 } 975 } 976 977 @Override 978 @DefinedBy(Api.COMPILER_TREE) 979 public Void visitLiteral(LiteralTree tree, Void ignore) { 980 markEnclosingTag(Flag.HAS_INLINE_TAG); 981 if (tree.getKind() == DocTree.Kind.CODE) { 982 for (TagStackItem tsi : tagStack) { 983 if (tsi.tag == HtmlTag.CODE) { 984 env.messages.warning(HTML, tree, "dc.tag.code.within.code"); 985 break; 986 } 987 } 988 } 989 return super.visitLiteral(tree, ignore); 990 } 991 992 @Override 993 @DefinedBy(Api.COMPILER_TREE) 994 public Void visitParam(ParamTree tree, Void ignore) { 995 boolean typaram = tree.isTypeParameter(); 996 IdentifierTree nameTree = tree.getName(); 997 Element paramElement = nameTree != null 998 ? env.trees.getElement(new DocTreePath(getCurrentPath(), nameTree)) 999 : null; 1000 1001 if (paramElement == null) { 1002 switch (env.currElement.getKind()) { 1003 case CLASS, INTERFACE -> { 1004 if (typaram) { 1005 env.messages.error(REFERENCE, nameTree, 1006 "dc.param.name.not.found"); 1007 } else { 1008 env.messages.error(REFERENCE, tree, "dc.invalid.param"); 1009 } 1010 } 1011 1012 case METHOD, CONSTRUCTOR, RECORD -> env.messages.error(REFERENCE, 1013 nameTree, "dc.param.name.not.found"); 1014 1015 default -> env.messages.error(REFERENCE, tree, "dc.invalid.param"); 1016 } 1017 } else { 1018 boolean unique = foundParams.add(paramElement); 1019 1020 if (!unique) { 1021 env.messages.warning(REFERENCE, tree, "dc.exists.param", 1022 nameTree); 1023 } 1024 } 1025 1026 warnIfEmpty(tree, tree.getDescription()); 1027 return super.visitParam(tree, ignore); 1028 } 1029 1030 private void checkParamsDocumented(List<? extends Element> list) { 1031 if (foundInheritDoc) 1032 return; 1033 1034 for (Element e : list) { 1035 if (!foundParams.contains(e)) { 1036 CharSequence paramName 1037 = (e.getKind() == ElementKind.TYPE_PARAMETER) 1038 ? "<" + e.getSimpleName() + ">" 1039 : e.getSimpleName(); 1040 reportMissing("dc.missing.param", paramName); 1041 } 1042 } 1043 } 1044 1045 @Override 1046 @DefinedBy(Api.COMPILER_TREE) 1047 public Void visitProvides(ProvidesTree tree, Void ignore) { 1048 Element e = env.trees.getElement(env.currPath); 1049 if (e.getKind() != ElementKind.MODULE) { 1050 env.messages.error(REFERENCE, tree, "dc.invalid.provides"); 1051 } 1052 ReferenceTree serviceType = tree.getServiceType(); 1053 Element se = env.trees 1054 .getElement(new DocTreePath(getCurrentPath(), serviceType)); 1055 if (se == null) { 1056 env.messages.error(REFERENCE, tree, "dc.service.not.found"); 1057 } 1058 return super.visitProvides(tree, ignore); 1059 } 1060 1061 @Override 1062 @DefinedBy(Api.COMPILER_TREE) 1063 public Void visitReference(ReferenceTree tree, Void ignore) { 1064 Element e = env.trees.getElement(getCurrentPath()); 1065 if (e == null) { 1066 reportBadReference(tree); 1067 } 1068 return super.visitReference(tree, ignore); 1069 } 1070 1071 private void reportBadReference(ReferenceTree tree) { 1072 if (!env.strictReferenceChecks) { 1073 String refSig = tree.getSignature(); 1074 int sep = refSig.indexOf("/"); 1075 if (sep > 0) { 1076 String moduleName = refSig.substring(0, sep); 1077 if (SourceVersion.isName(moduleName)) { 1078 Element m = env.elements.getModuleElement(moduleName); 1079 if (m == null) { 1080 env.messages.warning(REFERENCE, tree, 1081 "dc.ref.in.missing.module", moduleName); 1082 return; 1083 } 1084 } 1085 } 1086 } 1087 1088 env.messages.error(REFERENCE, tree, "dc.ref.not.found"); 1089 } 1090 1091 @Override 1092 @DefinedBy(Api.COMPILER_TREE) 1093 public Void visitReturn(ReturnTree tree, Void ignore) { 1094 if (foundReturn) { 1095 env.messages.warning(REFERENCE, tree, "dc.exists.return"); 1096 } 1097 if (tree.isInline()) { 1098 DocCommentTree dct = getCurrentPath().getDocComment(); 1099 if (dct.getFirstSentence().isEmpty() 1100 || tree != dct.getFirstSentence().get(0)) { 1101 env.messages.warning(SYNTAX, tree, "dc.return.not.first"); 1102 } 1103 } 1104 1105 Element e = env.trees.getElement(env.currPath); 1106 if (e.getKind() != ElementKind.METHOD 1107 || ((ExecutableElement) e).getReturnType() 1108 .getKind() == TypeKind.VOID) 1109 env.messages.error(REFERENCE, tree, "dc.invalid.return"); 1110 foundReturn = true; 1111 warnIfEmpty(tree, tree.getDescription()); 1112 return super.visitReturn(tree, ignore); 1113 } 1114 1115 @Override 1116 @DefinedBy(Api.COMPILER_TREE) 1117 public Void visitSerialData(SerialDataTree tree, Void ignore) { 1118 warnIfEmpty(tree, tree.getDescription()); 1119 return super.visitSerialData(tree, ignore); 1120 } 1121 1122 @Override 1123 @DefinedBy(Api.COMPILER_TREE) 1124 public Void visitSerialField(SerialFieldTree tree, Void ignore) { 1125 warnIfEmpty(tree, tree.getDescription()); 1126 return super.visitSerialField(tree, ignore); 1127 } 1128 1129 @Override 1130 @DefinedBy(Api.COMPILER_TREE) 1131 public Void visitSince(SinceTree tree, Void ignore) { 1132 warnIfEmpty(tree, tree.getBody()); 1133 return super.visitSince(tree, ignore); 1134 } 1135 1136 @Override 1137 @DefinedBy(Api.COMPILER_TREE) 1138 public Void visitSummary(SummaryTree tree, Void aVoid) { 1139 markEnclosingTag(Flag.HAS_INLINE_TAG); 1140 if (inSummary) { 1141 env.messages.warning(HTML, tree, "dc.tag.nested.tag", 1142 "@" + tree.getTagName()); 1143 } 1144 int idx = env.currDocComment.getFullBody().indexOf(tree); 1145 // Warn if the node is preceded by non-whitespace characters, 1146 // or other non-text nodes. 1147 if ((idx == 1 && hasNonWhitespaceText) || idx > 1) { 1148 env.messages.warning(SYNTAX, tree, "dc.invalid.summary", 1149 tree.getTagName()); 1150 } 1151 boolean prevInSummary = inSummary; 1152 try { 1153 inSummary = true; 1154 return super.visitSummary(tree, aVoid); 1155 } finally { 1156 inSummary = prevInSummary; 1157 } 1158 } 1159 1160 @Override 1161 @DefinedBy(Api.COMPILER_TREE) 1162 public Void visitSystemProperty(SystemPropertyTree tree, Void ignore) { 1163 markEnclosingTag(Flag.HAS_INLINE_TAG); 1164 for (TagStackItem tsi : tagStack) { 1165 if (tsi.tag == HtmlTag.A) { 1166 env.messages.warning(HTML, tree, "dc.tag.a.within.a", 1167 "{@" + tree.getTagName() + "}"); 1168 break; 1169 } 1170 } 1171 return super.visitSystemProperty(tree, ignore); 1172 } 1173 1174 @Override 1175 @DefinedBy(Api.COMPILER_TREE) 1176 public Void visitThrows(ThrowsTree tree, Void ignore) { 1177 ReferenceTree exName = tree.getExceptionName(); 1178 Element ex 1179 = env.trees.getElement(new DocTreePath(getCurrentPath(), exName)); 1180 if (ex == null) { 1181 env.messages.error(REFERENCE, tree, "dc.ref.not.found"); 1182 } else if (isThrowable(ex.asType())) { 1183 switch (env.currElement.getKind()) { 1184 case CONSTRUCTOR, METHOD -> { 1185 if (isCheckedException(ex.asType())) { 1186 ExecutableElement ee = (ExecutableElement) env.currElement; 1187 checkThrowsDeclared(exName, ex.asType(), 1188 ee.getThrownTypes()); 1189 } 1190 } 1191 1192 default -> env.messages.error(REFERENCE, tree, "dc.invalid.throws"); 1193 } 1194 } else { 1195 env.messages.error(REFERENCE, tree, "dc.invalid.throws"); 1196 } 1197 warnIfEmpty(tree, tree.getDescription()); 1198 return scan(tree.getDescription(), ignore); 1199 } 1200 1201 private boolean isThrowable(TypeMirror tm) { 1202 return switch (tm.getKind()) { 1203 case DECLARED, TYPEVAR -> env.types.isAssignable(tm, 1204 env.java_lang_Throwable); 1205 default -> false; 1206 }; 1207 } 1208 1209 private void checkThrowsDeclared(ReferenceTree tree, TypeMirror t, 1210 List<? extends TypeMirror> list) { 1211 boolean found = false; 1212 for (TypeMirror tl : list) { 1213 if (env.types.isAssignable(t, tl)) { 1214 foundThrows.add(tl); 1215 found = true; 1216 } 1217 } 1218 if (!found) 1219 env.messages.error(REFERENCE, tree, "dc.exception.not.thrown", t); 1220 } 1221 1222 private void checkThrowsDocumented(List<? extends TypeMirror> list) { 1223 if (foundInheritDoc) 1224 return; 1225 1226 for (TypeMirror tl : list) { 1227 if (isCheckedException(tl) && !foundThrows.contains(tl)) 1228 reportMissing("dc.missing.throws", tl); 1229 } 1230 } 1231 1232 @Override 1233 @DefinedBy(Api.COMPILER_TREE) 1234 public Void visitUnknownBlockTag(UnknownBlockTagTree tree, Void ignore) { 1235 checkUnknownTag(tree, tree.getTagName()); 1236 return super.visitUnknownBlockTag(tree, ignore); 1237 } 1238 1239 @Override 1240 @DefinedBy(Api.COMPILER_TREE) 1241 public Void visitUnknownInlineTag(UnknownInlineTagTree tree, Void ignore) { 1242 markEnclosingTag(Flag.HAS_INLINE_TAG); 1243 checkUnknownTag(tree, tree.getTagName()); 1244 return super.visitUnknownInlineTag(tree, ignore); 1245 } 1246 1247 private void checkUnknownTag(DocTree tree, String tagName) { 1248 if (env.customTags != null && !env.customTags.contains(tagName)) 1249 env.messages.error(SYNTAX, tree, "dc.tag.unknown", tagName); 1250 } 1251 1252 @Override 1253 @DefinedBy(Api.COMPILER_TREE) 1254 public Void visitUses(UsesTree tree, Void ignore) { 1255 Element e = env.trees.getElement(env.currPath); 1256 if (e.getKind() != ElementKind.MODULE) { 1257 env.messages.error(REFERENCE, tree, "dc.invalid.uses"); 1258 } 1259 ReferenceTree serviceType = tree.getServiceType(); 1260 Element se = env.trees 1261 .getElement(new DocTreePath(getCurrentPath(), serviceType)); 1262 if (se == null) { 1263 env.messages.error(REFERENCE, tree, "dc.service.not.found"); 1264 } 1265 return super.visitUses(tree, ignore); 1266 } 1267 1268 @Override 1269 @DefinedBy(Api.COMPILER_TREE) 1270 public Void visitValue(ValueTree tree, Void ignore) { 1271 ReferenceTree ref = tree.getReference(); 1272 if (ref == null || ref.getSignature().isEmpty()) { 1273 if (!isConstant(env.currElement)) 1274 env.messages.error(REFERENCE, tree, 1275 "dc.value.not.allowed.here"); 1276 } else { 1277 Element e 1278 = env.trees.getElement(new DocTreePath(getCurrentPath(), ref)); 1279 if (!isConstant(e)) 1280 env.messages.error(REFERENCE, tree, "dc.value.not.a.constant"); 1281 } 1282 TextTree format = tree.getFormat(); 1283 if (format != null) { 1284 String f = format.getBody().toString(); 1285 long count = format.getBody().toString().chars() 1286 .filter(ch -> ch == '%') 1287 .count(); 1288 if (count != 1) { 1289 env.messages.error(REFERENCE, format, "dc.value.bad.format", f); 1290 } 1291 } 1292 1293 markEnclosingTag(Flag.HAS_INLINE_TAG); 1294 return super.visitValue(tree, ignore); 1295 } 1296 1297 private boolean isConstant(Element e) { 1298 if (e != null && e.getKind() == ElementKind.FIELD) { 1299 Object value = ((VariableElement) e).getConstantValue(); 1300 return (value != null); // can't distinguish "not a constant" from 1301 // "constant is null" 1302 } else { 1303 return false; 1304 } 1305 } 1306 1307 @Override 1308 @DefinedBy(Api.COMPILER_TREE) 1309 public Void visitVersion(VersionTree tree, Void ignore) { 1310 warnIfEmpty(tree, tree.getBody()); 1311 return super.visitVersion(tree, ignore); 1312 } 1313 1314 @Override 1315 @DefinedBy(Api.COMPILER_TREE) 1316 public Void visitErroneous(ErroneousTree tree, Void ignore) { 1317 env.messages.error(SYNTAX, tree, null, 1318 tree.getDiagnostic().getMessage(null)); 1319 return null; 1320 } 1321 // </editor-fold> 1322 1323 // <editor-fold defaultstate="collapsed" desc="Utility methods"> 1324 1325 private DocTree.Kind getParentKind() { 1326 return getCurrentPath().getParentPath().getLeaf().getKind(); 1327 } 1328 1329 private boolean isCheckedException(TypeMirror t) { 1330 return !(env.types.isAssignable(t, env.java_lang_Error) 1331 || env.types.isAssignable(t, env.java_lang_RuntimeException)); 1332 } 1333 1334 private boolean isSynthetic() { 1335 return env.elements 1336 .getOrigin(env.currElement) == Elements.Origin.SYNTHETIC; 1337 } 1338 1339 private boolean isAnonymous() { 1340 return (env.currElement instanceof TypeElement te) 1341 && te.getNestingKind() == NestingKind.ANONYMOUS; 1342 } 1343 1344 private boolean isDefaultConstructor() { 1345 if (env.currElement.getKind() == ElementKind.CONSTRUCTOR) { 1346 // A synthetic default constructor has the same pos as the 1347 // enclosing class 1348 TreePath p = env.currPath; 1349 return env.getPos(p) == env.getPos(p.getParentPath()); 1350 } else { 1351 return false; 1352 } 1353 } 1354 1355 private boolean isDeclaredType() { 1356 ElementKind ek = env.currElement.getKind(); 1357 return ek.isClass() || ek.isInterface(); 1358 } 1359 1360 private boolean isExecutable() { 1361 ElementKind ek = env.currElement.getKind(); 1362 return switch (ek) { 1363 case CONSTRUCTOR, METHOD -> true; 1364 default -> false; 1365 }; 1366 } 1367 1368 private boolean isRecordComponentOrField() { 1369 return env.currElement.getKind() == ElementKind.RECORD_COMPONENT 1370 || env.currElement.getEnclosingElement() != null 1371 && env.currElement.getEnclosingElement() 1372 .getKind() == ElementKind.RECORD 1373 && env.currElement.getKind() == ElementKind.FIELD; 1374 } 1375 1376 private boolean isNormalClass(TreePath p) { 1377 return switch (p.getLeaf().getKind()) { 1378 case ENUM, RECORD -> false; 1379 case CLASS -> true; 1380 default -> throw new IllegalArgumentException( 1381 p.getLeaf().getKind().name()); 1382 }; 1383 } 1384 1385 void markEnclosingTag(Flag flag) { 1386 TagStackItem top = tagStack.peek(); 1387 if (top != null) 1388 top.flags.add(flag); 1389 } 1390 1391 // for debug use 1392 String toString(TreePath p) { 1393 StringBuilder sb = new StringBuilder("TreePath["); 1394 toString(p, sb); 1395 sb.append("]"); 1396 return sb.toString(); 1397 } 1398 1399 void toString(TreePath p, StringBuilder sb) { 1400 TreePath parent = p.getParentPath(); 1401 if (parent != null) { 1402 toString(parent, sb); 1403 sb.append(","); 1404 } 1405 sb.append(p.getLeaf().getKind()).append(":").append(env.getPos(p)) 1406 .append(":S").append(env.getStartPos(p)); 1407 } 1408 1409 void warnIfEmpty(DocTree tree, List<? extends DocTree> list) { 1410 for (DocTree d : list) { 1411 if (d.getKind() != DocTree.Kind.TEXT 1412 || hasNonWhitespace((TextTree) d)) { 1413 return; 1414 } 1415 } 1416 env.messages.warning(MISSING, tree, "dc.empty", tree.getKind().tagName); 1417 } 1418 1419 boolean hasNonWhitespace(TextTree tree) { 1420 return !tree.getBody().isBlank(); 1421 } 1422 1423 // </editor-fold> 1424 1425}