DefaultDataContextPlugin.java

/* Contributed in the public domain.
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.compiler.plugin;

import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import java.util.EnumSet;
import java.util.Set;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import org.orekit.annotation.DefaultDataContext;

/**
 * Processes {@link DefaultDataContext} to issue warnings at compile time.
 *
 * <p>To use this plugin add {@code -Xplugin:dataContextPlugin} to the javac command line.
 * Tested with OpenJDK 8 and 11.
 *
 * <p>Do not reference this class unless executing within {@code javac} or you have added
 * {@code tools.jar} to the class path. {@code tools.jar} is part of the JDK, not JRE, and
 * is typically located at {@code JAVA_HOME/../lib/tools.jar}.
 *
 * @author Evan Ward
 * @since 10.1
 */
@SupportedAnnotationTypes("org.orekit.annotation.DefaultDataContext")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DefaultDataContextPlugin implements Plugin, TaskListener {

    /** Warning message. */
    static final String MESSAGE = "Use of the default data context from a scope not " +
            "annotated with @DefaultDataContext. This code may be unintentionally " +
            "using the default data context.";
    /** Annotation to search for. */
    private static final Class<DefaultDataContext> ANNOTATION = DefaultDataContext.class;

    /** Compiler Trees. */
    private Trees trees;

    /** Empty constructor.
     * <p>
     * This constructor is not strictly necessary, but it prevents spurious
     * javadoc warnings with JDK 18 and later.
     * </p>
     * @since 12.0
     */
    public DefaultDataContextPlugin() {
        // nothing to do
    }

    @Override
    public String getName() {
        return "dataContextPlugin";
    }

    @Override
    public synchronized void init(final JavacTask javacTask, final String... args) {
        javacTask.addTaskListener(this);
        trees = Trees.instance(javacTask);
    }

    @Override
    public void started(final TaskEvent taskEvent) {
    }

    @Override
    public void finished(final TaskEvent taskEvent) {
        if (taskEvent.getKind() == Kind.ANALYZE) {
            final CompilationUnitTree root = taskEvent.getCompilationUnit();
            root.accept(new AnnotationTreeScanner(root), null);
        }
    }

    /** Finds when an annotation is used and checks the scope has the same annotation. */
    private class AnnotationTreeScanner extends TreeScanner<Void, Void> {

        /** Compilation root. */
        private final CompilationUnitTree root;

        /**
         * Create a scanner.
         *
         * @param root of the compilation.
         */
        AnnotationTreeScanner(final CompilationUnitTree root) {
            this.root = root;
        }

        @Override
        public Void visitIdentifier(final IdentifierTree identifierTree,
                                    final Void aVoid) {
            check(identifierTree);
            return super.visitIdentifier(identifierTree, aVoid);
        }

        @Override
        public Void visitMemberSelect(final MemberSelectTree memberSelectTree,
                                      final Void aVoid) {
            check(memberSelectTree);
            return super.visitMemberSelect(memberSelectTree, aVoid);
        }

        @Override
        public Void visitNewClass(final NewClassTree newClassTree, final Void aVoid) {
            check(newClassTree);
            return super.visitNewClass(newClassTree, aVoid);
        }

        /**
         * Check if this bit of code calls into the default data context from outside the
         * default data context.
         *
         * @param tree to check.
         */
        private void check(final Tree tree) {
            final Element element = trees.getElement(trees.getPath(root, tree));
            check(tree, element);
        }

        /**
         * Check tricky bits of code.
         *
         * @param tree    used to check the containing scope and for logging.
         * @param element to check for {@link #ANNOTATION}.
         */
        private void check(final Tree tree, final Element element) {
            // element and its containing scopes.
            if (isAnyElementAnnotated(element)) {
                // using code annotated with @DefaultDataContext
                // check if current scope is also annotated
                if (!isAnyElementAnnotated(trees.getPath(root, tree))) {
                    // calling the default data context from a method without an annotation
                    final String message = MESSAGE + " Used: " + element.getKind() + " " + element;
                    trees.printMessage(Diagnostic.Kind.WARNING, message, tree, root);
                }
            }
        }

        /**
         * Determine if any enclosing element has {@link #ANNOTATION}. Walks towards the
         * tree root checking each node for the annotation.
         *
         * @param element to start the search from. May be {@code null}.
         * @return {@code true} if {@code element} or any of its parents are annotated,
         * {@code false} otherwise.
         */
        private boolean isAnyElementAnnotated(final Element element) {
            Element e = element;
            while (e != null) {
                if (e.getAnnotation(ANNOTATION) != null) {
                    return true;
                }
                e = e.getEnclosingElement();
            }
            return false;
        }

        /**
         * Determine if any enclosing tree has {@link #ANNOTATION}. Walks towards the tree
         * root checking each node for the annotation.
         *
         * @param path to start the search from. May be {@code null}.
         * @return {@code true} if {@code path} or any of its parents are annotated,
         * {@code false} otherwise.
         */
        private boolean isAnyElementAnnotated(final TreePath path) {
            // Kinds of declarations which can be annotated
            final Set<Tree.Kind> toCheck = EnumSet.of(
                    Tree.Kind.METHOD, Tree.Kind.CLASS, Tree.Kind.VARIABLE,
                    Tree.Kind.INTERFACE, Tree.Kind.ENUM);
            TreePath next = path;
            while (next != null) {
                if (toCheck.contains(next.getLeaf().getKind())) {
                    if (trees.getElement(next).getAnnotation(ANNOTATION) != null) {
                        return true;
                    }
                }
                next = next.getParentPath();
            }
            return false;
        }

    }

}