/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.jackpot.cmds;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.netbeans.api.jackpot.QueryContext;
import org.netbeans.api.jackpot.QueryOperations;
import org.netbeans.api.jackpot.TreePathTransformer;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.ElementUtilities;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.api.java.source.WorkingCopy;
import org.openide.ErrorManager;
/**
* Minimizes method access modifiers based on project usage.
This transformer
* demonstrates two-pass operation within the Java Source framework. The first
* pass locates all method declarations and invocations, storing them in a
* map using persistent tree and element references. This first pass is started
* directly by this transformer in its init() method, which is
* invoked prior to transformer execution. The second pass runs as a
* regular transformation, looking up each compilation unit's methods to see
* what modifications it needs and applies them. The ordering of these two
* passes is important, because only modifications made by the regular
* transformation pass are displayed to the user and committed to the source files.
*/
public class MinimizeMethodAccess extends TreePathTransformer {
private JavaSource javaSource;
private Map,MethodReferences> refs;
private static final int PUBLIC_USE = 1;
private static final int PACKAGE_USE = 2;
private static final int CLASS_USE = 4;
// property set by PropertySheetInfo
private boolean convertPackagePrivate = true;
public boolean getConvertPackagePrivate() {
return convertPackagePrivate;
}
public void setConvertPackagePrivate(boolean b) {
convertPackagePrivate = b;
}
@Override
public void init(QueryContext context, JavaSource javaSource) {
super.init(context, javaSource);
this.javaSource = javaSource;
refs = new HashMap,MethodReferences>();
scanMethods();
}
@Override
public void destroy() {
refs = null;
super.destroy();
}
private void scanMethods() {
final ReferenceScanner scanner = new ReferenceScanner();
CancellableTask task = new CancellableTask() {
private boolean cancelled = false;
public void run(CompilationController cc) throws IOException {
if (!cancelled) {
cc.toPhase(JavaSource.Phase.RESOLVED);
scanner.scan(cc.getCompilationUnit(), cc);
}
}
public void cancel() {
cancelled = true;
}
};
try {
javaSource.runUserActionTask(task, false);
} catch (IOException e) {
ErrorManager.getDefault().notify(e);
}
}
@Override
public Void visitMethod(MethodTree t, Object p) {
super.visitMethod(t, p);
WorkingCopy wc = getWorkingCopy();
Trees trees = wc.getTrees();
ExecutableElement meth = (ExecutableElement)trees.getElement(getCurrentPath());
MethodReferences methodRefs = meth != null ? refs.get(ElementHandle.create(meth)) : null;
if (methodRefs == null) // true if method isn't "interesting"
return null;
Set flags = meth.getModifiers();
Set newFlags = EnumSet.noneOf(Modifier.class);
newFlags.addAll(flags);
TypeElement owner = (TypeElement)meth.getEnclosingElement();
if (flags.contains(Modifier.PUBLIC)) {
if (noPublicUsage(methodRefs))
newFlags.remove(Modifier.PUBLIC);
else if (onlyProtectedUsage(wc, owner, methodRefs)) {
newFlags.remove(Modifier.PUBLIC);
newFlags.add(Modifier.PROTECTED);
}
}
else if (flags.contains(Modifier.PROTECTED) && !onlyProtectedUsage(wc, owner, methodRefs)) {
newFlags.remove(Modifier.PROTECTED);
}
if (convertPackagePrivate &&
!newFlags.contains(Modifier.PUBLIC) &&
!newFlags.contains(Modifier.PROTECTED) &&
!flags.contains(Modifier.PRIVATE) &&
!flags.contains(Modifier.ABSTRACT) &&
noPublicUsage(methodRefs) &&
noPackageUsage(methodRefs))
newFlags.add(Modifier.PRIVATE);
if (!flags.equals(newFlags)) {
// Make sure overriding methods don't weaken access.
ExecutableElement parentMethod = wc.getElementUtilities().getOverriddenMethod(meth);
if (parentMethod == null || // true if method doesn't override
access(newFlags) >= access(parentMethod.getModifiers())) {
ModifiersTree mods = t.getModifiers();
ModifiersTree newMods = wc.getTreeMaker().Modifiers(newFlags, mods.getAnnotations());
StringBuilder sb = new StringBuilder();
String s = mods.toString();
sb.append(s.length() > 0 ? s : "package-private");
sb.append(" => ");
s = newMods.toString();
sb.append(s.length() > 0 ? s : "package-private");
addChange(new TreePath(getCurrentPath(), mods), newMods, sb.toString());
}
}
return null;
}
/**
* See if usage is limited to subclasses.
*/
private boolean onlyProtectedUsage(WorkingCopy wc, Element owner, MethodReferences methodRefs) {
Types types = wc.getTypes();
TypeMirror classType = owner.asType();
for (Reference r : methodRefs.references) {
TypeMirror referenceOwner = MinimizeFieldAccess.getClassType(r.element.resolve(wc));
if (referenceOwner != classType && !types.isSubtype(referenceOwner, classType))
return false;
}
return true;
}
private static boolean noPublicUsage(MethodReferences mr) {
return (mr.usage & PUBLIC_USE) == 0;
}
private static boolean noPackageUsage(MethodReferences mr) {
return (mr.usage & PACKAGE_USE) == 0;
}
/** Returns a comparable int corresponding to the flags' access level */
private int access(Set flags) {
if (flags.contains(Modifier.PUBLIC))
return 4;
if (flags.contains(Modifier.PROTECTED))
return 2;
return flags.contains(Modifier.PRIVATE) ? 0 : 1;
}
private Element getReferringElement(TreePath path, CompilationInfo info) {
Element e = info.getTrees().getElement(path);
return e != null ? e : getReferringElement(path.getParentPath(), info);
}
/**
* A description of a references to a variable element.
*/
private static class MethodReferences {
ExecutableElement var;
TreePathHandle declaration;
int usage;
List references;
// these elements are only valid when the ReferenceScanner is scanning
TypeElement cls;
PackageElement pkg;
MethodReferences(ExecutableElement var) {
this.var = var;
cls = QueryOperations.getDeclaringClass(var);
pkg = QueryOperations.getDeclaringPackage(cls);
references = new ArrayList();
}
}
/**
* A reference to an element by a single tree node.
*/
private static class Reference {
ElementHandle element;
TreePathHandle tree;
}
/**
* A scanner which finds all variable declarations and references in a project.
*/
private class ReferenceScanner extends TreePathScanner {
private TypeElement currentClass;
private PackageElement currentPackage;
private boolean isInteresting(Element sym, TreePath path, CompilationInfo info) {
if (sym == null || !(sym instanceof ExecutableElement))
return false;
// ignore compiler-generated methods
if (info.getElementUtilities().isSynthetic(sym))
return false;
SourcePositions srcPos = info.getTrees().getSourcePositions();
CompilationUnitTree unit = path.getCompilationUnit();
long treePos = srcPos.getStartPosition(unit, path.getLeaf());
long parentPos = srcPos.getStartPosition(unit, path.getParentPath().getLeaf());
if (treePos == parentPos) // true for default constructors
return false;
// ignore enum and interface methods
TypeElement owner = (TypeElement)sym.getEnclosingElement();
if (owner.getKind() == ElementKind.ENUM || owner.getKind() == ElementKind.INTERFACE)
return false;
// ignore methods in anonymous classes, since they aren't accessible
if (owner.getNestingKind() == NestingKind.ANONYMOUS)
return false;
// ignore non-public methods if possible
Set flags = sym.getModifiers();
if (!convertPackagePrivate &&
!flags.contains(Modifier.PUBLIC) && !flags.contains(Modifier.PROTECTED))
return false;
// ignore public no-arg constructors, which are often invoked by reflection
ExecutableElement ee = (ExecutableElement)sym;
if (sym.getKind() == ElementKind.CONSTRUCTOR &&
flags.contains(Modifier.PUBLIC) &&
ee.getParameters().isEmpty())
return false;
// ignore any methods which override their superclass or implement interface methods.
ElementUtilities utils = info.getElementUtilities();
if (utils.overridesMethod(ee) || utils.implementsMethod(ee))
return false;
// ignore main methods
if (flags.contains(Modifier.PUBLIC) &&
flags.contains(Modifier.STATIC) &&
"void".equals(ee.getReturnType().toString()) &&
"main".equals(ee.getSimpleName().toString()))
return false;
return true;
}
private void add(CompilationInfo info) {
TreePath path = getCurrentPath();
Element e = info.getTrees().getElement(path);
if (!isInteresting(e, path, info))
return;
assert e instanceof ExecutableElement;
ExecutableElement var = (ExecutableElement)e;
TreePathHandle handle = TreePathHandle.create(path, info);
MethodReferences uses = refs.get(var);
if (uses == null) {
uses = new MethodReferences(var);
if (path.getLeaf() instanceof MethodTree)
uses.declaration = handle;
refs.put(ElementHandle.create(var), uses);
}
Reference ref = new Reference();
ref.element = ElementHandle.create(getReferringElement(path, info));
ref.tree = handle;
uses.references.add(ref);
int flags = currentClass == uses.cls ? CLASS_USE
: currentPackage == uses.pkg ? PACKAGE_USE
: PUBLIC_USE;
uses.usage |= flags;
}
@Override
public Void visitClass(ClassTree tree, CompilationInfo info) {
currentClass = (TypeElement)info.getTrees().getElement(getCurrentPath());
currentPackage = info.getElements().getPackageOf(currentClass);
super.visitClass(tree, info);
currentClass = null;
currentPackage = null;
return null;
}
@Override
public Void visitMethod(MethodTree tree, CompilationInfo info) {
Element e = info.getTrees().getElement(getCurrentPath());
add(info);
return super.visitMethod(tree, info);
}
@Override
public Void visitMethodInvocation(MethodInvocationTree tree, CompilationInfo info) {
Element e = info.getTrees().getElement(getCurrentPath());
add(info);
return super.visitMethodInvocation(tree, info);
}
@Override
public Void visitNewClass(NewClassTree tree, CompilationInfo info) {
Element e = info.getTrees().getElement(getCurrentPath());
add(info);
return super.visitNewClass(tree, info);
}
}
}