001/*
002 * Copyright (c) 1997, 2022, 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.doclets.toolkit;
027
028import java.io.File;
029import java.io.IOException;
030import java.nio.file.InvalidPathException;
031import java.nio.file.Path;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.LinkedHashSet;
038import java.util.List;
039import java.util.Locale;
040import java.util.Map;
041import java.util.Set;
042import java.util.SortedMap;
043import java.util.SortedSet;
044import java.util.TreeMap;
045import java.util.TreeSet;
046import java.util.function.Function;
047
048import javax.lang.model.SourceVersion;
049import javax.lang.model.element.Element;
050import javax.lang.model.element.ModuleElement;
051import javax.lang.model.element.PackageElement;
052import javax.lang.model.element.TypeElement;
053import javax.lang.model.util.Elements;
054import javax.lang.model.util.SimpleElementVisitor14;
055import javax.tools.DocumentationTool;
056import javax.tools.JavaFileManager;
057import javax.tools.JavaFileObject;
058import javax.tools.StandardJavaFileManager;
059
060import org.jdrupes.mdoclet.internal.doclets.toolkit.builders.BuilderFactory;
061import org.jdrupes.mdoclet.internal.doclets.toolkit.taglets.TagletManager;
062import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Comparators;
063import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFile;
064import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFileFactory;
065import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Extern;
066import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Group;
067import org.jdrupes.mdoclet.internal.doclets.toolkit.util.MetaKeywords;
068import org.jdrupes.mdoclet.internal.doclets.toolkit.util.SimpleDocletException;
069import org.jdrupes.mdoclet.internal.doclets.toolkit.util.TypeElementCatalog;
070import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils;
071import org.jdrupes.mdoclet.internal.doclets.toolkit.util.VisibleMemberCache;
072import org.jdrupes.mdoclet.internal.doclets.toolkit.util.VisibleMemberTable;
073import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils.Pair;
074import org.jdrupes.mdoclet.internal.doclint.DocLint;
075import org.jdrupes.mdoclet.internal.doclint.Env;
076
077import com.sun.source.tree.CompilationUnitTree;
078import com.sun.source.util.TreePath;
079import com.sun.tools.javac.util.DefinedBy;
080import com.sun.tools.javac.util.DefinedBy.Api;
081import jdk.javadoc.doclet.Doclet;
082import jdk.javadoc.doclet.DocletEnvironment;
083import jdk.javadoc.doclet.Reporter;
084import jdk.javadoc.doclet.StandardDoclet;
085import jdk.javadoc.doclet.Taglet;
086
087/**
088 * Configure the output based on the options. Doclets should subclass
089 * BaseConfiguration, to configure and add their own options. This class contains
090 * all user options which are supported by the standard doclet.
091 */
092public abstract class BaseConfiguration {
093    /**
094     * The doclet that created this configuration.
095     */
096    public final Doclet doclet;
097
098    /**
099     * The factory for builders.
100     */
101    protected BuilderFactory builderFactory;
102
103    /**
104     * The taglet manager.
105     */
106    public TagletManager tagletManager;
107
108    /**
109     * The meta tag keywords instance.
110     */
111    public MetaKeywords metakeywords;
112
113    /**
114     * The doclet environment.
115     */
116    public DocletEnvironment docEnv;
117
118    /**
119     * An utility class for commonly used helpers
120     */
121    public Utils utils;
122
123    /**
124     * All the temporary accessors to javac internals.
125     */
126    public WorkArounds workArounds;
127
128    /**
129     * Sourcepath from where to read the source files. Default is classpath.
130     */
131    public String sourcepath = "";
132
133    /**
134     * Generate modules documentation if more than one module is present.
135     */
136    public boolean showModules = false;
137
138    /**
139     * The catalog of classes specified on the command-line
140     */
141    public TypeElementCatalog typeElementCatalog;
142
143    /**
144     * The package grouping instance.
145     */
146    public final Group group = new Group(this);
147
148    /**
149     * The tracker of external package links.
150     */
151    public Extern extern;
152
153    public final Reporter reporter;
154
155    public final Locale locale;
156
157    public abstract Messages getMessages();
158
159    public abstract Resources getDocResources();
160
161    /**
162     * Returns the version of the {@link #doclet doclet}.
163     *
164     * @return the version
165     */
166    public abstract Runtime.Version getDocletVersion();
167
168    /**
169     * This method should be defined in all those doclets (configurations),
170     * which want to derive themselves from this BaseConfiguration. This method
171     * can be used to finish up the options setup.
172     *
173     * @return true if successful and false otherwise
174     */
175
176    public abstract boolean finishOptionSettings();
177
178    public CommentUtils cmtUtils;
179
180    /**
181     * A sorted set of included packages.
182     */
183    public SortedSet<PackageElement> packages = null;
184
185    public OverviewElement overviewElement;
186
187    public DocFileFactory docFileFactory;
188
189    /**
190     * A sorted map, giving the (specified|included|other) packages for each module.
191     */
192    public SortedMap<ModuleElement, Set<PackageElement>> modulePackages;
193
194    /**
195     * The list of known modules, that should be documented.
196     */
197    public SortedSet<ModuleElement> modules;
198
199    protected static final String sharedResourceBundleName
200        = "org.jdrupes.mdoclet.internal.doclets.toolkit.resources.doclets";
201
202    private VisibleMemberCache visibleMemberCache;
203
204    public PropertyUtils propertyUtils = null;
205
206    /**
207     * Constructs the format-independent configuration needed by the doclet.
208     *
209     * @apiNote The {@code doclet} parameter is used when
210     * {@link Taglet#init(DocletEnvironment, Doclet) initializing tags}.
211     * Some doclets (such as the {@link StandardDoclet}), may delegate to another
212     * (such as the {@code HtmlDoclet}).  In such cases, the primary doclet (i.e
213     * {@code StandardDoclet}) should be provided here, and not any internal
214     * class like {@code HtmlDoclet}.
215     *
216     * @param doclet   the doclet for this run of javadoc
217     * @param locale   the locale for the generated documentation
218     * @param reporter the reporter to use for console messages
219     */
220    public BaseConfiguration(Doclet doclet, Locale locale, Reporter reporter) {
221        this.doclet = doclet;
222        this.locale = locale;
223        this.reporter = reporter;
224    }
225
226    public abstract BaseOptions getOptions();
227
228    private boolean initialized = false;
229
230    protected void initConfiguration(DocletEnvironment docEnv,
231            Function<String, String> resourceKeyMapper) {
232        if (initialized) {
233            throw new IllegalStateException(
234                "configuration previously initialized");
235        }
236        initialized = true;
237        this.docEnv = docEnv;
238        // Utils needs docEnv, safe to init now.
239        utils = new Utils(this);
240
241        BaseOptions options = getOptions();
242        if (!options.javafx()) {
243            options.setJavaFX(isJavaFXMode());
244        }
245
246        getDocResources().setKeyMapper(resourceKeyMapper);
247
248        // Once docEnv and Utils have been initialized, others should be safe.
249        metakeywords = new MetaKeywords(this);
250        cmtUtils = new CommentUtils(this);
251        workArounds = new WorkArounds(this);
252        visibleMemberCache = new VisibleMemberCache(this);
253        propertyUtils = new PropertyUtils(this);
254
255        Splitter specifiedSplitter = new Splitter(docEnv, false);
256        specifiedModuleElements
257            = Collections.unmodifiableSet(specifiedSplitter.mset);
258        specifiedPackageElements
259            = Collections.unmodifiableSet(specifiedSplitter.pset);
260        specifiedTypeElements
261            = Collections.unmodifiableSet(specifiedSplitter.tset);
262
263        Splitter includedSplitter = new Splitter(docEnv, true);
264        includedModuleElements
265            = Collections.unmodifiableSet(includedSplitter.mset);
266        includedPackageElements
267            = Collections.unmodifiableSet(includedSplitter.pset);
268        includedTypeElements
269            = Collections.unmodifiableSet(includedSplitter.tset);
270    }
271
272    /**
273     * Return the builder factory for this doclet.
274     *
275     * @return the builder factory for this doclet.
276     */
277    public BuilderFactory getBuilderFactory() {
278        if (builderFactory == null) {
279            builderFactory = new BuilderFactory(this);
280        }
281        return builderFactory;
282    }
283
284    public Reporter getReporter() {
285        return this.reporter;
286    }
287
288    private Set<ModuleElement> specifiedModuleElements;
289
290    public Set<ModuleElement> getSpecifiedModuleElements() {
291        return specifiedModuleElements;
292    }
293
294    private Set<PackageElement> specifiedPackageElements;
295
296    public Set<PackageElement> getSpecifiedPackageElements() {
297        return specifiedPackageElements;
298    }
299
300    private Set<TypeElement> specifiedTypeElements;
301
302    public Set<TypeElement> getSpecifiedTypeElements() {
303        return specifiedTypeElements;
304    }
305
306    private Set<ModuleElement> includedModuleElements;
307
308    public Set<ModuleElement> getIncludedModuleElements() {
309        return includedModuleElements;
310    }
311
312    private Set<PackageElement> includedPackageElements;
313
314    public Set<PackageElement> getIncludedPackageElements() {
315        return includedPackageElements;
316    }
317
318    private Set<TypeElement> includedTypeElements;
319
320    public Set<TypeElement> getIncludedTypeElements() {
321        return includedTypeElements;
322    }
323
324    private void initModules() {
325        Comparators comparators = utils.comparators;
326        // Build the modules structure used by the doclet
327        modules = new TreeSet<>(comparators.makeModuleComparator());
328        modules.addAll(getSpecifiedModuleElements());
329
330        modulePackages = new TreeMap<>(comparators.makeModuleComparator());
331        for (PackageElement p : packages) {
332            ModuleElement mdle = docEnv.getElementUtils().getModuleOf(p);
333            if (mdle != null && !mdle.isUnnamed()) {
334                Set<PackageElement> s = modulePackages
335                    .computeIfAbsent(mdle, m -> new TreeSet<>(
336                        comparators.makePackageComparator()));
337                s.add(p);
338            }
339        }
340
341        for (PackageElement p : getIncludedPackageElements()) {
342            ModuleElement mdle = docEnv.getElementUtils().getModuleOf(p);
343            if (mdle != null && !mdle.isUnnamed()) {
344                Set<PackageElement> s = modulePackages
345                    .computeIfAbsent(mdle, m -> new TreeSet<>(
346                        comparators.makePackageComparator()));
347                s.add(p);
348            }
349        }
350
351        // add entries for modules which may not have exported packages
352        modules.forEach(
353            mdle -> modulePackages.computeIfAbsent(mdle, m -> Set.of()));
354
355        modules.addAll(modulePackages.keySet());
356        showModules = !modules.isEmpty();
357        for (Set<PackageElement> pkgs : modulePackages.values()) {
358            packages.addAll(pkgs);
359        }
360    }
361
362    private void initPackages() {
363        packages = new TreeSet<>(utils.comparators.makePackageComparator());
364        // add all the included packages
365        packages.addAll(includedPackageElements);
366    }
367
368    /*
369     * when this is called all the option have been set, this method,
370     * initializes certain components before anything else is started.
371     */
372    protected boolean finishOptionSettings0() throws DocletException {
373        BaseOptions options = getOptions();
374        extern = new Extern(this);
375        initDestDirectory();
376        for (String link : options.linkList()) {
377            extern.link(link, reporter);
378        }
379        for (Pair<String, String> linkOfflinePair : options.linkOfflineList()) {
380            extern.link(linkOfflinePair.first, linkOfflinePair.second,
381                reporter);
382        }
383        if (!options.noPlatformLinks()) {
384            extern.checkPlatformLinks(options.linkPlatformProperties(),
385                reporter);
386        }
387        typeElementCatalog = new TypeElementCatalog(includedTypeElements, this);
388
389        String snippetPath = options.snippetPath();
390        if (snippetPath != null) {
391            Messages messages = getMessages();
392            JavaFileManager fm = getFileManager();
393            if (fm instanceof StandardJavaFileManager) {
394                try {
395                    List<Path> sp
396                        = Arrays.stream(snippetPath.split(File.pathSeparator))
397                            .map(Path::of)
398                            .toList();
399                    StandardJavaFileManager sfm = (StandardJavaFileManager) fm;
400                    sfm.setLocationFromPaths(
401                        DocumentationTool.Location.SNIPPET_PATH, sp);
402                } catch (IOException | InvalidPathException e) {
403                    throw new SimpleDocletException(
404                        messages.getResources().getText(
405                            "doclet.error_setting_snippet_path", snippetPath,
406                            e),
407                        e);
408                }
409            } else {
410                throw new SimpleDocletException(messages.getResources().getText(
411                    "doclet.cannot_use_snippet_path", snippetPath));
412            }
413        }
414
415        initTagletManager(options.customTagStrs());
416        options.groupPairs().forEach(grp -> {
417            if (showModules) {
418                group.checkModuleGroups(grp.first, grp.second);
419            } else {
420                group.checkPackageGroups(grp.first, grp.second);
421            }
422        });
423
424        PackageElement unnamedPackage;
425        Elements elementUtils = utils.elementUtils;
426        if (docEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_9) >= 0) {
427            ModuleElement unnamedModule = elementUtils.getModuleElement("");
428            unnamedPackage = elementUtils.getPackageElement(unnamedModule, "");
429        } else {
430            unnamedPackage = elementUtils.getPackageElement("");
431        }
432        overviewElement
433            = new OverviewElement(unnamedPackage, getOverviewPath());
434        return true;
435    }
436
437    /**
438     * Set the command-line options supported by this configuration.
439     *
440     * @return true if the options are set successfully
441     * @throws DocletException if there is a problem while setting the options
442     */
443    public boolean setOptions() throws DocletException {
444        initPackages();
445        initModules();
446        return finishOptionSettings0()
447            && finishOptionSettings();
448    }
449
450    private void initDestDirectory() throws DocletException {
451        String destDirName = getOptions().destDirName();
452        if (!destDirName.isEmpty()) {
453            Messages messages = getMessages();
454            DocFile destDir = DocFile.createFileForDirectory(this, destDirName);
455            if (!destDir.exists()) {
456                // Create the output directory (in case it doesn't exist yet)
457                messages.notice("doclet.dest_dir_create", destDirName);
458                destDir.mkdirs();
459            } else if (!destDir.isDirectory()) {
460                throw new SimpleDocletException(messages.getResources().getText(
461                    "doclet.destination_directory_not_directory_0",
462                    destDir.getPath()));
463            } else if (!destDir.canWrite()) {
464                throw new SimpleDocletException(messages.getResources().getText(
465                    "doclet.destination_directory_not_writable_0",
466                    destDir.getPath()));
467            }
468        }
469        DocFileFactory.getFactory(this).setDestDir(destDirName);
470    }
471
472    /**
473     * Initialize the taglet manager.  The strings to initialize the simple custom tags should
474     * be in the following format:  "[tag name]:[location str]:[heading]".
475     *
476     * @param customTagStrs the set two dimensional arrays of strings.  These arrays contain
477     *                      either -tag or -taglet arguments.
478     */
479    private void initTagletManager(Set<List<String>> customTagStrs) {
480        tagletManager
481            = tagletManager != null ? tagletManager : new TagletManager(this);
482        JavaFileManager fileManager = getFileManager();
483        Messages messages = getMessages();
484        try {
485            tagletManager.initTagletPath(fileManager);
486            tagletManager.loadTaglets(fileManager);
487
488            for (List<String> args : customTagStrs) {
489                if (args.get(0).equals("-taglet")) {
490                    tagletManager.addCustomTag(args.get(1), fileManager);
491                    continue;
492                }
493                /*
494                 * Since there are few constraints on the characters in a tag
495                 * name,
496                 * and real world examples with ':' in the tag name, we cannot
497                 * simply use
498                 * String.split(regex); instead, we tokenize the string,
499                 * allowing
500                 * special characters to be escaped with '\'.
501                 */
502                List<String> tokens = tokenize(args.get(1), 3);
503                switch (tokens.size()) {
504                case 1 -> {
505                    String tagName = args.get(1);
506                    if (tagletManager.isKnownCustomTag(tagName)) {
507                        // reorder a standard tag
508                        tagletManager.addNewSimpleCustomTag(tagName, null, "");
509                    } else {
510                        // Create a simple tag with the heading that has the
511                        // same name as the tag.
512                        StringBuilder heading
513                            = new StringBuilder(tagName + ":");
514                        heading.setCharAt(0,
515                            Character.toUpperCase(tagName.charAt(0)));
516                        tagletManager.addNewSimpleCustomTag(tagName,
517                            heading.toString(), "a");
518                    }
519                }
520
521                case 2 ->
522                        // Add simple taglet without heading, probably to
523                        // excluding it in the output.
524                        tagletManager.addNewSimpleCustomTag(tokens.get(0),
525                            tokens.get(1), "");
526
527                case 3 -> tagletManager.addNewSimpleCustomTag(tokens.get(0),
528                    tokens.get(2), tokens.get(1));
529
530                default -> messages.error(
531                    "doclet.Error_invalid_custom_tag_argument", args.get(1));
532                }
533            }
534        } catch (IOException e) {
535            messages.error("doclet.taglet_could_not_set_location",
536                e.toString());
537        }
538    }
539
540    /**
541     * Given a string, return an array of tokens, separated by ':'.
542     * The separator character can be escaped with the '\' character.
543     * The '\' character may also be escaped with the '\' character.
544     *
545     * @param s         the string to tokenize
546     * @param maxTokens the maximum number of tokens returned.  If the
547     *                  max is reached, the remaining part of s is appended
548     *                  to the end of the last token.
549     * @return an array of tokens
550     */
551    private List<String> tokenize(String s, int maxTokens) {
552        List<String> tokens = new ArrayList<>();
553        StringBuilder token = new StringBuilder();
554        boolean prevIsEscapeChar = false;
555        for (int i = 0; i < s.length(); i += Character.charCount(i)) {
556            int currentChar = s.codePointAt(i);
557            if (prevIsEscapeChar) {
558                // Case 1: escaped character
559                token.appendCodePoint(currentChar);
560                prevIsEscapeChar = false;
561            } else if (currentChar == ':' && tokens.size() < maxTokens - 1) {
562                // Case 2: separator
563                tokens.add(token.toString());
564                token = new StringBuilder();
565            } else if (currentChar == '\\') {
566                // Case 3: escape character
567                prevIsEscapeChar = true;
568            } else {
569                // Case 4: regular character
570                token.appendCodePoint(currentChar);
571            }
572        }
573        if (token.length() > 0) {
574            tokens.add(token.toString());
575        }
576        return tokens;
577    }
578
579    /**
580     * Return true if the given doc-file subdirectory should be excluded and
581     * false otherwise.
582     *
583     * @param docfilesubdir the doc-files subdirectory to check.
584     * @return true if the directory is excluded.
585     */
586    public boolean shouldExcludeDocFileDir(String docfilesubdir) {
587        Set<String> excludedDocFileDirs = getOptions().excludedDocFileDirs();
588        return excludedDocFileDirs.contains(docfilesubdir);
589    }
590
591    /**
592     * Return true if the given qualifier should be excluded and false otherwise.
593     *
594     * @param qualifier the qualifier to check.
595     * @return true if the qualifier should be excluded
596     */
597    public boolean shouldExcludeQualifier(String qualifier) {
598        Set<String> excludedQualifiers = getOptions().excludedQualifiers();
599        if (excludedQualifiers.contains("all") ||
600            excludedQualifiers.contains(qualifier) ||
601            excludedQualifiers.contains(qualifier + ".*")) {
602            return true;
603        } else {
604            int index = -1;
605            while ((index = qualifier.indexOf(".", index + 1)) != -1) {
606                if (excludedQualifiers
607                    .contains(qualifier.substring(0, index + 1) + "*")) {
608                    return true;
609                }
610            }
611            return false;
612        }
613    }
614
615    /**
616     * Return the qualified name of the Element if its qualifier is not excluded.
617     * Otherwise return the unqualified Element name.
618     *
619     * @param te the TypeElement to check.
620     * @return the class name
621     */
622    public String getClassName(TypeElement te) {
623        PackageElement pkg = utils.containingPackage(te);
624        return shouldExcludeQualifier(utils.getPackageName(pkg))
625            ? utils.getSimpleName(te)
626            : utils.getFullyQualifiedName(te);
627    }
628
629    /**
630     * Return true if the TypeElement element is getting documented, depending upon
631     * -nodeprecated option and the deprecation information. Return true if
632     * -nodeprecated is not used. Return false if -nodeprecated is used and if
633     * either TypeElement element is deprecated or the containing package is deprecated.
634     *
635     * @param te the TypeElement for which the page generation is checked
636     * @return true if it is a generated doc.
637     */
638    public boolean isGeneratedDoc(TypeElement te) {
639        boolean nodeprecated = getOptions().noDeprecated();
640        if (!nodeprecated) {
641            return true;
642        }
643        return !(utils.isDeprecated(te)
644            || utils.isDeprecated(utils.containingPackage(te)));
645    }
646
647    /**
648     * Return the doclet specific instance of a writer factory.
649     *
650     * @return the {@link WriterFactory} for the doclet.
651     */
652    public abstract WriterFactory getWriterFactory();
653
654    /**
655     * Return the Locale for this document.
656     *
657     * @return the current locale
658     */
659    public abstract Locale getLocale();
660
661    /**
662     * Return the path of the overview file and null if it does not exist.
663     *
664     * @return the path of the overview file.
665     */
666    public abstract JavaFileObject getOverviewPath();
667
668    /**
669     * Return the current file manager.
670     *
671     * @return JavaFileManager
672     */
673    public abstract JavaFileManager getFileManager();
674
675    /*
676     * Splits the elements in a collection to its individual
677     * collection.
678     */
679    private static class Splitter {
680
681        final Set<ModuleElement> mset = new LinkedHashSet<>();
682        final Set<PackageElement> pset = new LinkedHashSet<>();
683        final Set<TypeElement> tset = new LinkedHashSet<>();
684
685        Splitter(DocletEnvironment docEnv, boolean included) {
686
687            Set<? extends Element> inset = included
688                ? docEnv.getIncludedElements()
689                : docEnv.getSpecifiedElements();
690
691            for (Element e : inset) {
692                new SimpleElementVisitor14<Void, Void>() {
693                    @Override
694                    @DefinedBy(Api.LANGUAGE_MODEL)
695                    public Void visitModule(ModuleElement e, Void p) {
696                        mset.add(e);
697                        return null;
698                    }
699
700                    @Override
701                    @DefinedBy(Api.LANGUAGE_MODEL)
702                    public Void visitPackage(PackageElement e, Void p) {
703                        pset.add(e);
704                        return null;
705                    }
706
707                    @Override
708                    @DefinedBy(Api.LANGUAGE_MODEL)
709                    public Void visitType(TypeElement e, Void p) {
710                        tset.add(e);
711                        return null;
712                    }
713
714                    @Override
715                    @DefinedBy(Api.LANGUAGE_MODEL)
716                    protected Void defaultAction(Element e, Void p) {
717                        throw new AssertionError("unexpected element: " + e);
718                    }
719
720                }.visit(e);
721            }
722        }
723    }
724
725    /**
726     * Returns whether or not to allow JavaScript in comments.
727     * Default is off; can be set true from a command-line option.
728     *
729     * @return the allowScriptInComments
730     */
731    public boolean isAllowScriptInComments() {
732        return getOptions().allowScriptInComments();
733    }
734
735    public VisibleMemberTable getVisibleMemberTable(TypeElement te) {
736        return visibleMemberCache.getVisibleMemberTable(te);
737    }
738
739    /**
740     * Determines if JavaFX is available in the compilation environment.
741     * @return true if JavaFX is available
742     */
743    public boolean isJavaFXMode() {
744        TypeElement observable
745            = utils.elementUtils.getTypeElement("javafx.beans.Observable");
746        if (observable == null) {
747            return false;
748        }
749        ModuleElement javafxModule = utils.elementUtils.getModuleOf(observable);
750        return javafxModule == null
751            || javafxModule.isUnnamed()
752            || javafxModule.getQualifiedName().contentEquals("javafx.base");
753    }
754
755    // <editor-fold desc="DocLint support">
756
757    private DocLint doclint;
758
759    Map<CompilationUnitTree, Boolean> shouldCheck = new HashMap<>();
760
761    public void runDocLint(TreePath path) {
762        CompilationUnitTree unit = path.getCompilationUnit();
763        if (doclint != null
764            && shouldCheck.computeIfAbsent(unit, doclint::shouldCheck)) {
765            doclint.scan(path);
766        }
767    }
768
769    /**
770     * Initializes DocLint, if appropriate, depending on options derived
771     * from the doclet command-line options, and the set of custom tags
772     * that should be ignored by DocLint.
773     *
774     * DocLint is not enabled if the option {@code -Xmsgs:none} is given,
775     * and it is not followed by any options to enable any groups.
776     * Note that arguments for {@code -Xmsgs:} can be given individually
777     * in separate {@code -Xmsgs:} options, or in a comma-separated list
778     * for a single option. For example, the following are equivalent:
779     * <ul>
780     *     <li>{@code -Xmsgs:all} {@code -Xmsgs:-html}
781     *     <li>{@code -Xmsgs:all,-html}
782     * </ul>
783     *
784     * @param opts  options for DocLint, derived from the corresponding doclet
785     *              command-line options
786     * @param customTagNames the names of custom tags, to be ignored by doclint
787     */
788    public void initDocLint(List<String> opts, Set<String> customTagNames) {
789        List<String> doclintOpts = new ArrayList<>();
790
791        // basic analysis of -Xmsgs and -Xmsgs: options to see if doclint is
792        // enabled
793        Set<String> groups = new HashSet<>();
794        boolean seenXmsgs = false;
795        for (String opt : opts) {
796            if (opt.equals(DocLint.XMSGS_OPTION)) {
797                groups.add("all");
798                seenXmsgs = true;
799            } else if (opt.startsWith(DocLint.XMSGS_CUSTOM_PREFIX)) {
800                String[] args
801                    = opt.substring(DocLint.XMSGS_CUSTOM_PREFIX.length())
802                        .split(DocLint.SEPARATOR);
803                for (String a : args) {
804                    if (a.equals("none")) {
805                        groups.clear();
806                    } else if (a.startsWith("-")) {
807                        groups.remove(a.substring(1));
808                    } else {
809                        groups.add(a);
810                    }
811                }
812                seenXmsgs = true;
813            }
814            doclintOpts.add(opt);
815        }
816
817        if (seenXmsgs) {
818            if (groups.isEmpty()) {
819                // no groups enabled; do not init doclint
820                return;
821            }
822        } else {
823            // no -Xmsgs options of any kind, use default
824            doclintOpts.add(DocLint.XMSGS_OPTION);
825        }
826
827        if (!customTagNames.isEmpty()) {
828            String customTags = String.join(DocLint.SEPARATOR, customTagNames);
829            doclintOpts.add(DocLint.XCUSTOM_TAGS_PREFIX + customTags);
830        }
831
832        doclint = new DocLint();
833        doclint.init(docEnv.getDocTrees(), docEnv.getElementUtils(),
834            docEnv.getTypeUtils(),
835            doclintOpts.toArray(new String[0]));
836    }
837
838    public boolean isDocLintReferenceGroupEnabled() {
839        return isDocLintGroupEnabled(
840            org.jdrupes.mdoclet.internal.doclint.Messages.Group.REFERENCE);
841    }
842
843    public boolean isDocLintSyntaxGroupEnabled() {
844        return isDocLintGroupEnabled(
845            org.jdrupes.mdoclet.internal.doclint.Messages.Group.SYNTAX);
846    }
847
848    private boolean isDocLintGroupEnabled(
849            org.jdrupes.mdoclet.internal.doclint.Messages.Group group) {
850        // Use AccessKind.PUBLIC as a stand-in, since it is not common to
851        // set DocLint options per access kind (as is common with javac.)
852        // A more sophisticated solution might be to derive the access kind from
853        // the
854        // element owning the comment, and its enclosing elements.
855        return doclint != null
856            && doclint.isGroupEnabled(group, Env.AccessKind.PUBLIC);
857    }
858    // </editor-fold>
859}