All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.massfords.javac.effectivelyfinal.EffectivelyFinalVisitor Maven / Gradle / Ivy

package com.massfords.javac.effectivelyfinal;

import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.tools.Diagnostic;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Visitor that traverses the compilation unit in search of assignments to params
 */
public class EffectivelyFinalVisitor extends TreePathScanner> {

    /**
     * Utilities for reporting errors and working with the task
     */
    private final Trees trees;

    /**
     * The compilation unit we're visiting.
     */
    private CompilationUnitTree compilationUnitTree;

    public EffectivelyFinalVisitor(JavacTask task) {
        trees = Trees.instance(task);
    }

    /**
     * Keeps track of the compilation unit since it's needed to report errors
     * @param node unit we're compiling
     * @param names set of non-final param names in scope
     */
    @Override
    public Void visitCompilationUnit(CompilationUnitTree node, Set names) {
        this.compilationUnitTree = node;
        return super.visitCompilationUnit(node, names);
    }

    /**
     * If we're visiting a method, we want to record any of its non-final params and if we have one
     * or more non-final param then we want to traverse into the body and hit every assignment.
     *
     * We're relying on javac to report any real errors and thus our logic here can be simpler.
     * For example, when visiting a method, we can ignore any context passed into us since any params in scope
     * from enclosing methods must already be explicitly or effectively final.
     *
     * Consider the following:
     *
     * void foo(int a, int b) {
     *
     *     MyClass mc = new MyClass() {
     *         void differentMethod(int c, int d) {
     *             a = c + d; // :--- we don't need to check for this since javac will catch for us
     *         }
     *     }
     * }
     *
     * Therefore, when we encounter a method like we have here, we can disregard the param given to us and
     * traverse into the body of the method with all of the non-final params from just this method.
     *
     * It's tempting to not traverse if all of the params are final or it has no params but there could
     * be something in the body like an inner class that has an assignment to one of its own params
     *
     * @param node method we're visiting
     * @param nonFinalParamsInScope params will be ignored since we'll reset when we scan from here
     */
    @Override
    public Void visitMethod(MethodTree node, Set nonFinalParamsInScope) {
        Set nonFinalParams = node.getParameters()
                .stream()
                .filter(varTree -> !varTree.getModifiers().getFlags().contains(Modifier.FINAL))
                .map(VariableTree::getName)
                .distinct()
                .collect(Collectors.toSet());

        scan(node.getBody(), nonFinalParams);
        return null;
    }

    /**
     * If we're assigning to an identifier then we see if this identifier has the same name as one of the
     * non-final params in scope. We only care about assignments to an identifier.
     *
     * Thus, a = 5; would be flagged by array[0] = "foo" would not be flagged. The left hand side of the assignment
     * operation must be an identifier in order for us to flag it.
     *
     * @param assignmentTree assignment AST node
     * @param nonFinalParamsInScope params to check against the LHS of the assignment
     */
    @Override
    public Void visitAssignment(AssignmentTree assignmentTree, Set nonFinalParamsInScope) {
        if (nonFinalParamsInScope != null && !nonFinalParamsInScope.isEmpty()) {
            ExpressionTree variable = assignmentTree.getVariable();
            variable.accept(new SimpleTreeVisitor() {
                @Override
                public Void visitIdentifier(IdentifierTree node, Void aVoid) {
                    if (nonFinalParamsInScope.contains(node.getName())) {
                        // printing a message of type error counts as a compilation error
                        trees.printMessage(Diagnostic.Kind.ERROR,
                                String.format("EFFECTIVELY_FINAL: Assignment to param in `%s`", assignmentTree),
                                node, compilationUnitTree);
                    }
                    return null;
                }

            }, null);
        }
        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy