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}