001/*
002 * Copyright (c) 2012, 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.doclint;
027
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collections;
031import java.util.EnumSet;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.LinkedHashSet;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.function.Function;
039import java.util.regex.Pattern;
040import java.util.stream.Collectors;
041
042import javax.lang.model.element.AnnotationMirror;
043import javax.lang.model.element.AnnotationValue;
044import javax.lang.model.element.Element;
045import javax.lang.model.element.ElementKind;
046import javax.lang.model.element.ExecutableElement;
047import javax.lang.model.element.Modifier;
048import javax.lang.model.type.DeclaredType;
049import javax.lang.model.type.TypeKind;
050import javax.lang.model.type.TypeMirror;
051import javax.lang.model.util.Elements;
052import javax.lang.model.util.Types;
053
054import com.sun.source.doctree.DocCommentTree;
055import com.sun.source.tree.CompilationUnitTree;
056import com.sun.source.util.DocTrees;
057import com.sun.source.util.JavacTask;
058import com.sun.source.util.SourcePositions;
059import com.sun.source.util.TreePath;
060import com.sun.tools.javac.model.JavacTypes;
061import com.sun.tools.javac.tree.JCTree;
062import com.sun.tools.javac.util.MatchingUtils;
063import com.sun.tools.javac.util.StringUtils;
064
065/**
066 * Utility container for current execution environment,
067 * providing the current declaration and its doc comment.
068 */
069public class Env {
070    /**
071     * Access kinds for declarations.
072     */
073    public enum AccessKind {
074        PRIVATE,
075        PACKAGE,
076        PROTECTED,
077        PUBLIC;
078
079        static boolean accepts(String opt) {
080            for (AccessKind g : values())
081                if (opt.equals(StringUtils.toLowerCase(g.name())))
082                    return true;
083            return false;
084        }
085
086        static AccessKind of(Set<Modifier> mods) {
087            if (mods.contains(Modifier.PUBLIC))
088                return AccessKind.PUBLIC;
089            else if (mods.contains(Modifier.PROTECTED))
090                return AccessKind.PROTECTED;
091            else if (mods.contains(Modifier.PRIVATE))
092                return AccessKind.PRIVATE;
093            else
094                return AccessKind.PACKAGE;
095        }
096    }
097
098    /** Message handler. */
099    final Messages messages;
100
101    Set<String> customTags;
102
103    Set<Pattern> includePackages;
104    Set<Pattern> excludePackages;
105
106    /**
107     * How to handle bad references.
108     *
109     * If {@code false}, a reference into a module that is not
110     * in the module graph will just be reported as a warning.
111     * All other bad references will be reported as errors.
112     * This is the desired behavior for javac.
113     *
114     * If {@code true}, all bad references will be reported as
115     * errors. This is the desired behavior for javadoc.
116     *
117     */
118    boolean strictReferenceChecks = false;
119
120    // Utility classes
121    DocTrees trees;
122    Elements elements;
123    Types types;
124
125    // Types used when analysing doc comments.
126    TypeMirror java_lang_Error;
127    TypeMirror java_lang_RuntimeException;
128    TypeMirror java_lang_SuppressWarnings;
129    TypeMirror java_lang_Throwable;
130    TypeMirror java_lang_Void;
131
132    /** The path for the declaration containing the comment currently being analyzed. */
133    TreePath currPath;
134    /** The element for the declaration containing the comment currently being analyzed. */
135    Element currElement;
136    /** The comment current being analyzed. */
137    DocCommentTree currDocComment;
138    /**
139     * The access kind of the declaration containing the comment currently being analyzed.
140     * This is the minimum (most restrictive) access kind of the declaration itself
141     * and that of its containers. For example, a public method in a private class is
142     * noted as private.
143     */
144    AccessKind currAccess;
145    /** The set of methods, if any, that the current declaration overrides. */
146    Set<? extends ExecutableElement> currOverriddenMethods;
147
148    /** A map containing the info derived from {@code @SuppressWarnings} for an element. */
149    Map<Element, Set<Messages.Group>> suppressWarnings = new HashMap<>();
150
151    Env() {
152        messages = new Messages(this);
153    }
154
155    void init(JavacTask task) {
156        init(DocTrees.instance(task), task.getElements(), task.getTypes());
157    }
158
159    void init(DocTrees trees, Elements elements, Types types) {
160        this.trees = trees;
161        this.elements = elements;
162        this.types = types;
163    }
164
165    void initTypes() {
166        if (java_lang_Error != null)
167            return;
168
169        java_lang_Error = elements.getTypeElement("java.lang.Error").asType();
170        java_lang_RuntimeException
171            = elements.getTypeElement("java.lang.RuntimeException").asType();
172        java_lang_SuppressWarnings
173            = elements.getTypeElement("java.lang.SuppressWarnings").asType();
174        java_lang_Throwable
175            = elements.getTypeElement("java.lang.Throwable").asType();
176        java_lang_Void = elements.getTypeElement("java.lang.Void").asType();
177    }
178
179    void setCustomTags(String cTags) {
180        customTags = new LinkedHashSet<>();
181        for (String s : cTags.split(DocLint.SEPARATOR)) {
182            if (!s.isEmpty())
183                customTags.add(s);
184        }
185    }
186
187    void setCheckPackages(String packages) {
188        includePackages = new HashSet<>();
189        excludePackages = new HashSet<>();
190        for (String pack : packages.split(DocLint.SEPARATOR)) {
191            boolean excluded = false;
192            if (pack.startsWith("-")) {
193                pack = pack.substring(1);
194                excluded = true;
195            }
196            if (pack.isEmpty())
197                continue;
198            Pattern pattern = MatchingUtils.validImportStringToPattern(pack);
199            if (excluded) {
200                excludePackages.add(pattern);
201            } else {
202                includePackages.add(pattern);
203            }
204        }
205    }
206
207    static boolean validatePackages(String packages) {
208        for (String pack : packages.split(DocLint.SEPARATOR)) {
209            if (pack.startsWith("-")) {
210                pack = pack.substring(1);
211            }
212            if (!pack.isEmpty() && !MatchingUtils.isValidImportString(pack))
213                return false;
214        }
215        return true;
216    }
217
218    /** Set the current declaration and its doc comment. */
219    void setCurrent(TreePath path, DocCommentTree comment) {
220        currPath = path;
221        currDocComment = comment;
222        currElement = trees.getElement(currPath);
223        currOverriddenMethods
224            = ((JavacTypes) types).getOverriddenMethods(currElement);
225
226        AccessKind ak = AccessKind.PUBLIC;
227        for (TreePath p = path; p != null; p = p.getParentPath()) {
228            Element e = trees.getElement(p);
229            if (e != null && e.getKind() != ElementKind.PACKAGE
230                && e.getKind() != ElementKind.MODULE) {
231                ak = min(ak, AccessKind.of(e.getModifiers()));
232            }
233        }
234        currAccess = ak;
235    }
236
237    AccessKind getAccessKind() {
238        return currAccess;
239    }
240
241    long getPos(TreePath p) {
242        return ((JCTree) p.getLeaf()).pos;
243    }
244
245    long getStartPos(TreePath p) {
246        SourcePositions sp = trees.getSourcePositions();
247        return sp.getStartPosition(p.getCompilationUnit(), p.getLeaf());
248    }
249
250    boolean shouldCheck(CompilationUnitTree unit) {
251        if (includePackages == null)
252            return true;
253
254        String packageName = unit.getPackageName() != null
255            ? unit.getPackageName().toString()
256            : "";
257
258        if (!includePackages.isEmpty()) {
259            boolean included = false;
260            for (Pattern pack : includePackages) {
261                if (pack.matcher(packageName).matches()) {
262                    included = true;
263                    break;
264                }
265            }
266            if (!included)
267                return false;
268        }
269
270        for (Pattern pack : excludePackages) {
271            if (pack.matcher(packageName).matches()) {
272                return false;
273            }
274        }
275
276        return true;
277    }
278
279    /**
280     * {@return whether or not warnings in a group are suppressed for the current element}
281     * @param g the group
282     */
283    boolean suppressWarnings(Messages.Group g) {
284        return suppressWarnings(currElement, g);
285    }
286
287    /**
288     * {@return whether or not warnings in a group are suppressed for a given element}
289     * @param e the element
290     * @param g the group
291     */
292    boolean suppressWarnings(Element e, Messages.Group g) {
293        // check if warnings are suppressed in any enclosing classes
294        Element encl = e.getEnclosingElement();
295        if (encl != null && encl.asType().getKind() == TypeKind.DECLARED) {
296            if (suppressWarnings(encl, g)) {
297                return true;
298            }
299        }
300
301        // check the local @SuppressWarnings annotation, caching the results
302        return suppressWarnings.computeIfAbsent(e, this::getSuppressedGroups)
303            .contains(g);
304    }
305
306    /**
307     * Returns the set of groups for an element for which messages should be suppressed.
308     * The set is determined by examining the arguments for any {@code @SuppressWarnings}
309     * annotation that may be present on the element.
310     * The supported strings are: "doclint" and "doclint:GROUP,..." for each GROUP
311     *
312     * @param e the element
313     * @return  the set
314     */
315    private Set<Messages.Group> getSuppressedGroups(Element e) {
316        var gMap = Arrays.stream(Messages.Group.values())
317            .collect(
318                Collectors.toMap(Messages.Group::optName, Function.identity()));
319        var set = EnumSet.noneOf(Messages.Group.class);
320        for (String arg : getSuppressWarningsValue(e)) {
321            if (arg.equals("doclint")) {
322                set = EnumSet.allOf(Messages.Group.class);
323                break;
324            } else if (arg.startsWith("doclint:")) {
325                final int len = "doclint:".length();
326                for (String a : arg.substring(len).split(",")) {
327                    Messages.Group argGroup = gMap.get(a);
328                    if (argGroup != null) {
329                        set.add(argGroup);
330                    }
331                }
332            }
333        }
334        return set;
335    }
336
337    /**
338     * Returns the list of values given to an instance of {@code @SuppressWarnings} for an element,
339     * or an empty list if there is no annotation.
340     *
341     * @param e the element
342     * @return the list
343     */
344    private List<String> getSuppressWarningsValue(Element e) {
345        for (AnnotationMirror am : e.getAnnotationMirrors()) {
346            DeclaredType dt = am.getAnnotationType();
347            if (types.isSameType(dt, java_lang_SuppressWarnings)) {
348                var values = am.getElementValues();
349                for (var entry : values.entrySet()) {
350                    if (entry.getKey().getSimpleName().contentEquals("value")) {
351                        AnnotationValue av = entry.getValue();
352                        if (av.getValue() instanceof List<?> list) {
353                            List<String> result = new ArrayList<>();
354                            for (var item : list) {
355                                if (item instanceof AnnotationValue avItem
356                                    && avItem.getValue() instanceof String s) {
357                                    result.add(s);
358                                }
359                            }
360                            return result;
361                        }
362                    }
363                }
364
365            }
366        }
367        return List.of();
368    }
369
370    private <T extends Comparable<T>> T min(T item1, T item2) {
371        return (item1 == null) ? item2
372            : (item2 == null) ? item1
373                : item1.compareTo(item2) <= 0 ? item1 : item2;
374    }
375}