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

org.pure4j.processor.PureChecklistHandler Maven / Gradle / Ivy

Go to download

Parses Byte-code to check the purity semantics defined using pure4j-core annotations

The newest version!
package org.pure4j.processor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.pure4j.Pure4J;
import org.pure4j.annotations.immutable.IgnoreImmutableTypeCheck;
import org.pure4j.annotations.pure.Enforcement;
import org.pure4j.annotations.pure.Pure;
import org.pure4j.annotations.pure.PurityType;
import org.pure4j.exception.ClassExpectingPureMethod;
import org.pure4j.exception.ClassHierarchyUsesBothTypesOfPurity;
import org.pure4j.exception.IncorrectPure4JImmutableCallException;
import org.pure4j.exception.MemberCantBeHydratedException;
import org.pure4j.exception.MissingImmutableParameterCheckException;
import org.pure4j.exception.PureMethodAccessesNonFinalFieldException;
import org.pure4j.exception.PureMethodAccessesNonImmutableFieldException;
import org.pure4j.exception.PureMethodCallsImpureException;
import org.pure4j.exception.PureMethodNotInProjectScopeException;
import org.pure4j.exception.PureMethodOnNonImmutableClassException;
import org.pure4j.exception.PureMethodParameterNotImmutableException;
import org.pure4j.exception.PureMethodReturnNotImmutableException;
import org.pure4j.model.CallInfo;
import org.pure4j.model.ClassInitHandle;
import org.pure4j.model.ConstructorHandle;
import org.pure4j.model.FieldHandle;
import org.pure4j.model.ImplementationHandle;
import org.pure4j.model.MemberHandle;
import org.pure4j.model.MethodHandle;
import org.pure4j.model.ProjectModel;
import org.pure4j.model.StackArgumentsHandle;
import org.springframework.asm.Opcodes;

/**
 * The responsibility of this class is to keep track of all the methods we want
 * to check for purity. The {@link PureMethod} implementations track each
 * individual call and self-check the purity. This is done on a per
 * method-declaration level.
 * 
 * @author robmoffat
 *
 */
public class PureChecklistHandler {

	public static final boolean IGNORE_EXCEPTION_CONSTRUCTION = true;
	public static final boolean IGNORE_EQUALS_PARAMETER_PURITY = true;
	public static final boolean IGNORE_TOSTRING_PURITY = true;
	public static final boolean IGNORE_ENUM_VALUES_PURITY = true;

	public class PureMethod {

		public MemberHandle declaration;
		public Enforcement implPurity;
		public Enforcement intfPurity;
		private Boolean pureImplementation = null;
		private Boolean pureInterface = null;
		private PurityType pt;
		private Set> usedIn = new LinkedHashSet>();

		public Set> getUsedIn() {
			return usedIn;
		}

		private PureMethod(MemberHandle declaration, Enforcement impl, Enforcement intf, PurityType pt) {
			super();
			this.declaration = declaration;
			this.implPurity = impl;
			this.intfPurity = intf;
			setupEnforcements(intf, impl);
			this.pt = pt;
		}

		protected void setupEnforcements(Enforcement intf, Enforcement impl) {
			if (impl == Enforcement.FORCE) {
				pureImplementation = true;
				pureInterface = true;
			} else if (impl == Enforcement.NOT_PURE) {
				pureImplementation = false;
				pureInterface = false;
			}
			
			if (intf == Enforcement.FORCE) {
				pureInterface = true;
			} else if (intf == Enforcement.NOT_PURE) {
				pureInterface = false;
			}
		}

		private PureMethod(String description, Enforcement impl, Enforcement intf) throws ClassNotFoundException {
			int firstDot = description.indexOf(".");
			int firstBracket = description.indexOf("(");
			String classPart = description.substring(0, firstDot);
			String methodPart = description.substring(firstDot + 1, firstBracket);
			String descPart = description.substring(firstBracket);
			this.declaration = (methodPart.equals("")) ? new ConstructorHandle(classPart, descPart, 0) : new MethodHandle(classPart, methodPart, descPart, 0);

			this.implPurity = impl;
			this.intfPurity = intf;
			setupEnforcements(intf, impl);
		}

		@Override
		public String toString() {
			return "   See:[declaration=" + declaration + "\n     impl=" + implPurity + "\n     intf=" + intfPurity + ", \n     purity=" + pt+", \n     usedIn=\n" + lines(usedIn, 10) + "       ]";
		}

		private String lines(Set> usedIn2, int i) {
			StringBuilder sb = new StringBuilder();
			for (Class class1 : usedIn2) {
				for (int j = 0; j < i; j++) {
					sb.append(' ');
				}
				sb.append(class1.toString());
				sb.append("\n");
			}
			
			return sb.toString();
		}
		
		public Boolean checkInterfacePurity(Callback cb, ProjectModel pm) {
			if ((pureInterface == null) && (checkInterface)) {
				pureInterface = true;
				
				if (!pm.withinModel(declaration.getClassName())) {
					return pureInterface;
				}
				
				if (IGNORE_EQUALS_PARAMETER_PURITY) {
					if (("equals".equals(declaration.getName())) && ("(Ljava/lang/Object;)Z".equals(declaration.getDesc()))) {
						return true;
					}
				}

				
				// check the signature of accessible pure method.
				// only public/package visible methods need to be checked.
				
				if (isAccessibleOutsideClass(declaration, cl, pm)) {
					Type[] genericTypes = declaration.getGenericTypes(cl);
					int argOffset = getArgOffset();
					for (int i = thisFieldSkip(); i < genericTypes.length; i++) {
						Type t = genericTypes[i];
						if (typeFailsImmutabilityCheck(cb, t)) {
							int argNo = i+ argOffset;
							if (!isRuntimeChecked(argNo, pm, cb)) {
								cb.registerError(new PureMethodParameterNotImmutableException(this, t));
								pureInterface = false;
								return false;
							}
						}
					}
					
					if (pt == PurityType.IMMUTABLE_VALUE) {
						if (!Modifier.isStatic(declaration.getModifiers(cl))) {
							CallInfo ci = pm.getOpcodes(declaration);
							if (ci.usesThis()) {
								// if we use 'this', then the class must be immutable.
								for (Class c : usedIn) {
									if (typeFailsImmutabilityCheck(cb, c)) {
										cb.registerError(new PureMethodOnNonImmutableClassException(this, c));
										pureInterface = false;
										return false;
									}
								}
							}
						}
					}
					
					
					if (pt == PurityType.MUTABLE_UNSHARED) {
						if (declaration instanceof MethodHandle) {
							if (declaration.getAnnotation(cl, IgnoreImmutableTypeCheck.class) == null) {
								Method m = ((MethodHandle) declaration).hydrate(cl);
								Type t = m.getGenericReturnType();
								if (typeFailsImmutabilityCheck(cb, t) && (!returnsOwnType()))  {
									pureInterface = isRuntimeReturnChecked(pm, cb);
								}
							}
						} 
					}
				}
			}
			
			return pureInterface;
		}

		public Boolean checkImplementationPurity(Callback cb, ProjectModel pm) {
			if ((pureImplementation == null) && (checkImpl)) {
				try {
					pureImplementation = true;

					if (!pm.withinModel(declaration.getClassName())) {
						// we can't confirm that the method is pure, unless it
						// has been forced already.
						if (isMarkedPure(declaration, cb)) {
							pureImplementation = true;
							return true;
						} else {
							cb.registerError(new PureMethodNotInProjectScopeException(this));
							pureImplementation = false;
							return false;
						}
					}
					
					CallInfo ci = pm.getOpcodes(declaration);
					if ((ci.getOpcodes() & Opcodes.ACC_SYNTHETIC) == Opcodes.ACC_SYNTHETIC) {
						// synthetics are always pure.
						pureImplementation = true;
						return true;
					}

					if (this.declaration.getDeclaringClass(cl).isInterface()) {
						pureImplementation = true;
						return true;
					}

					if (IGNORE_ENUM_VALUES_PURITY && declaration.getDeclaringClass(cl).isEnum() && declaration.getName().equals("values")) {
						return true;
					}

					if (declaration instanceof MethodHandle) {
						Method m = ((MethodHandle) declaration).hydrate(cl);
						if (Modifier.isAbstract(m.getModifiers())) {
							return true;
						}
					}
					
					List calls = pm.getCalls(declaration);
					for (MemberHandle mh : calls) {
						if (mh instanceof ImplementationHandle) {
							mh = ensureCorrectClass(mh);
							if ((IGNORE_TOSTRING_PURITY) && (mh.getName().equals("toString")) && (mh.getDeclaringClass().equals("java/lang/Object"))) {
								// we can skip this one
							} else if (!isMarkedPure(mh, cb)) {
								cb.registerError(new PureMethodCallsImpureException(this, mh));
								pureImplementation = false;
							}
						} else if (mh instanceof FieldHandle) {
							FieldHandle fieldHandle = (FieldHandle) mh;
							Field f = fieldHandle.hydrate(cl);
							boolean isStatic = Modifier.isStatic(f.getModifiers());
							boolean isFinal = Modifier.isFinal(f.getModifiers());
							
							if (isStatic) {
								if (!isFinal) {
									cb.registerError(new PureMethodAccessesNonFinalFieldException(this, fieldHandle));
									pureImplementation = false;
								}
									
								if (!immutables.typeIsMarked(f.getGenericType(), cb)) {
									if ((!forcedImmutable(f)) && (isAccessibleOutsideClass(fieldHandle, cl, pm))) {
										cb.registerError(new PureMethodAccessesNonImmutableFieldException(this, fieldHandle));
										pureImplementation = false;
									}
								}
							}
							
						} 
					}
				} catch (MemberCantBeHydratedException e) {
					cb.registerError(e);
					pureImplementation = false;
				}

				cb.send("Pure implementation: " + this);

			}
			return pureImplementation;
		}

		private boolean returnsOwnType() {
			String returnedType = declaration.getDesc().substring(declaration.getDesc().lastIndexOf(")"));
			return (returnedType.contains(declaration.getDeclaringClass()));
		}

		protected boolean typeFailsImmutabilityCheck(Callback cb, Type t) {
			if (intfPurity == Enforcement.CHECKED) {
				return !immutables.typeIsMarked(t, cb);
			} 
			
			return false;
		}

		private int thisFieldSkip() {
			if (declaration instanceof ConstructorHandle) {
				Constructor c = ((ConstructorHandle)declaration).hydrate(cl);
				int countOfThisFields = 0;	// for 'this'
				for (Field f : c.getDeclaringClass().getDeclaredFields()) {
					if (f.getName().startsWith("this$")) {
						countOfThisFields++;
					}
				}
				
				return countOfThisFields > 0 ? 1 : 0;
			}
			
			return 0;
		}
		
		private int getArgOffset() {
			if (declaration instanceof ConstructorHandle) {
				return 1;
			} else if (declaration instanceof ClassInitHandle) {
				return 0;
			} else {
				// method handle
				Method m = ((MethodHandle) declaration).hydrate(cl);
				if (Modifier.isStatic(m.getModifiers())) {
					return 0;
				} else {
					return 1;
				}
				
			}
		}

		private boolean forcedImmutable(Field f) {
			IgnoreImmutableTypeCheck iv = f.getAnnotation(IgnoreImmutableTypeCheck.class);
			if ((iv != null)) {
				return true;
			}

			return false;
		}

		private boolean isRuntimeReturnChecked(ProjectModel pm, Callback cb) {
			CallInfo ci = pm.getOpcodes(declaration);
			
			for (Object mh : ci.getMethodsBeforeReturns()) {
				if (mh instanceof Integer) {
					int lineNumber = (Integer) mh;
					cb.registerError(new PureMethodReturnNotImmutableException(this, lineNumber));
					return false;
				} else if (mh instanceof ImplementationHandle) {
					MemberHandle ch = (MemberHandle) mh;
					if (ch.getClassName().equals(org.springframework.asm.Type.getInternalName(Pure4J.class)) &&
						(ch.getName().equals("returnImmutable")) ) {
					// return ok.
					
					} else {
						cb.registerError(new PureMethodReturnNotImmutableException(this, ch.getLine()));
						return false;
					}
				}
			}
			
			return true;
		}
		
		/**
		 * Checks to see whether the developer has called the runtime check for
		 * immutability.
		 */
		private boolean isRuntimeChecked(int paramNo, ProjectModel pm, Callback cb) {
			List calls = pm.getCalls(declaration);
			if (isCovariantMethod(calls)) {
				return true;
			}
			boolean found = false;
			boolean checkTried = false;
			for (MemberHandle memberHandle : calls) {
				if (memberHandle instanceof StackArgumentsHandle) {
					StackArgumentsHandle icmh = (StackArgumentsHandle) memberHandle;
					if (icmh.getName().equals("immutable")) {
						checkTried = true;
						if (!icmh.isFirstCall()) {
							cb.registerError(new IncorrectPure4JImmutableCallException(this));
							return false;
						} else if (icmh.getLocalVariables().contains(paramNo)) {
							found = true;
						}
					} else if (icmh.getName().equals("unsupported")) {
						checkTried = true;
						found = true;
					} else if (icmh.getName().equals("")) {
						if (icmh.getLocalVariables().contains(paramNo)) {
							found = true;
						}
					}
				}
			}

			if (checkTried) {
				if (!found) {
					cb.registerError(new MissingImmutableParameterCheckException(this, paramNo));
					return false;
				}
			}

			return checkTried;
		}
		
		/**
		 * If this is a covariant return type implementation, we don't need to
		 * bother with checking, it'll be done in the more specific method.
		 */
		private boolean isCovariantMethod(List calls) {
			if (!(this.declaration instanceof MethodHandle)) {
				return false;
			}
			
			if ((calls.size() == 1) && (calls.get(0) instanceof MethodHandle)) {
				MethodHandle theCall = (MethodHandle) calls.get(0);
				if (theCall.getName().equals(this.declaration.getName())) {
					Method theCallMethod = theCall.hydrate(cl);
					Method fromMethod = ((MethodHandle) this.declaration).hydrate(cl);
					if (theCallMethod.getReturnType().isAssignableFrom(fromMethod.getReturnType())) {
						if (theCallMethod.getParameterTypes().length == fromMethod.getParameterTypes().length) {
							return true;
						}
					}
				}
			}

			return false;
		}
	}

	private ClassLoader cl;
	private ClassAnnotationCache immutables;
	private ClassAnnotationCache mutableUnshared;
	private boolean checkInterface;
	private boolean checkImpl;

	public PureChecklistHandler(ClassLoader cl, ClassAnnotationCache immutables, ClassAnnotationCache mutableUnshared, boolean intf, boolean impl) {
		super();
		this.cl = cl;
		this.immutables = immutables;
		this.mutableUnshared = mutableUnshared;
		this.checkInterface = intf;
		this.checkImpl = impl;
		loadPureLists();
	}

	private Map pureChecklist = new HashMap();

	public void loadPureLists() {
		try {
			load("/java-lang.pure");
			load("/java-builtins.pure");
			load("/java-extra.pure");
		} catch (Exception e) {
			throw new RuntimeException("Couldn't load the pure lists: ", e);
		}
	}

	private void load(String fileName) throws IOException, ClassNotFoundException {
		InputStream is = PureChecklistHandler.class.getResourceAsStream(fileName);
		BufferedReader br = new BufferedReader(new InputStreamReader(is));
		String line = br.readLine();
		while (line != null) {
			String[] parts = line.trim().split(" ");
			Enforcement impl = Enforcement.valueOf(parts[0]);
			PureMethod pureMethod = new PureMethod(parts[1], impl, null);
			pureChecklist.put(pureMethod.declaration, pureMethod);
			line = br.readLine();
		}
	}

	private static boolean isAnonymousInnerClass(String className) {
		String tail = className.substring(className.lastIndexOf("$")+1);
		boolean out = tail.matches("[0-9]*");
		//int mods = PureChecklistHandler.class.getModifiers();
		return out;
	}
	
	
	public boolean isMarkedPure(MemberHandle mh, Callback cb) {
		boolean staticMethod = false;
		if (mh instanceof MethodHandle) {
			staticMethod = mh.getName().equals("") || Modifier.isStatic(mh.getModifiers(cl));
		}
		
		if (pureChecklist.containsKey(mh)) {
			PureMethod pm = pureChecklist.get(mh);
			return pm.implPurity != Enforcement.NOT_PURE;
		}

		if (IGNORE_EQUALS_PARAMETER_PURITY) {
			if (("equals".equals(mh.getName())) && ("(Ljava/lang/Object;)Z".equals(mh.getDesc()))) {
				return true;
			}
		}
		
		if ((mh instanceof ConstructorHandle) && (isAnonymousInnerClass(mh.getClassName()))) {
			// because you can't add the pure annotation to anonymous inner classes, we assume it is pure.
			return true;
		}
			
		

		Pure p = mh.getAnnotation(cl, Pure.class);
		if (p != null) {
			return p.value() != Enforcement.NOT_PURE;
		}

		if (staticMethod) {
			if (isInnerClassAccessMethod(mh, cb)) {
				return true;
			}
			
			return false;
		}

		// interfaces now allowed as pure
		if (mh.getDeclaringClass(cl).isInterface()) {
			return true;
		}

		if (isMarkedPure(mh.getDeclaringClass(cl), cb)) {
			return true;
		}

		if (IGNORE_EXCEPTION_CONSTRUCTION) {
			if (Throwable.class.isAssignableFrom(mh.getDeclaringClass(cl))) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize so that we are calling the class where the method is declared.
	 */
	private MemberHandle ensureCorrectClass(MemberHandle mh) {
		if (mh instanceof MethodHandle) {
			Method m = ((MethodHandle)mh).hydrate(cl);
			MethodHandle m2 = new MethodHandle(m);
			return m2;
		}
		return mh;
	}

	private boolean isInnerClassAccessMethod(MemberHandle mh, Callback cb) {
		if (mh instanceof MethodHandle) {
			Method m = ((MethodHandle) mh).hydrate(cl);
			if ((m.getName().startsWith("access$")) && 
					(m.getParameterTypes().length >= 1)) {
				
				Type t = m.getGenericParameterTypes()[0];
				if (t instanceof Class) {
					return isMarkedPure((Class) t, cb);
				}
			}
		}	
		return false;
	}
	
	public boolean isMarkedPure(Class in, Callback cb) {
		if ((in == null) || (in == Object.class)) {
			return false;
		}

		if (immutables.classIsMarked(in, cb)) {
			return true;
		}
		
		if (mutableUnshared.classIsMarked(in, cb)) {
			return true;
		}
		
		Pure p = in.getAnnotation(Pure.class);
		if ((p != null) && (p.value() != Enforcement.NOT_PURE)) {
			return true;
		}

		for (Class intf : in.getInterfaces()) {
			if (isMarkedPure(intf, cb)) {
				return true;
			}
		}

		if (isMarkedPure(in.getSuperclass(), cb)) {
			return true;
		}

		return false;
	}

	public void addMethod(MemberHandle declaration, Enforcement impl, Enforcement intf, PurityType pt, Class usedIn, Callback cb) {
		PureMethod pm;
		if (pureChecklist.containsKey(declaration)) {
			pm = pureChecklist.get(declaration);
		} else {
			pm = new PureMethod(declaration, impl, intf, pt);
			pureChecklist.put(declaration, pm);
			cb.send("  - " + declaration + " " + impl);
		}

		if (pm.implPurity != impl) {
			if (pm.implPurity != Enforcement.FORCE) {
				cb.registerError(new ClassExpectingPureMethod(usedIn, pm));
			}
		}
		
		if (pm.implPurity != Enforcement.NOT_PURE) {
			if ((pm.pt != pt) && (pm.pt != null)) {
				cb.registerError(new ClassHierarchyUsesBothTypesOfPurity(pm, usedIn));
			}
		}

		pm.usedIn.add(usedIn);
	}

	public PureMethod getElementFor(MemberHandle mh) {
		return pureChecklist.get(mh);
	}

	public void doPureMethodChecks(Callback cb, ProjectModel pm) {
		for (PureMethod pureCandidate : pureChecklist.values()) {
			if (checkImpl) {
				pureCandidate.checkImplementationPurity(cb, pm);
			}
			if (checkInterface) {
				pureCandidate.checkInterfacePurity(cb, pm);
			}
		}
	}

	public Collection getMethodList() {
		return pureChecklist.values();
	}

	public static boolean isAccessibleOutsideClass(MemberHandle handle, ClassLoader cl, ProjectModel pm) {
		boolean pub = Modifier.isPublic(handle.getModifiers(cl));
		boolean priv = Modifier.isPrivate(handle.getModifiers(cl));
		boolean prot = Modifier.isProtected(handle.getModifiers(cl));
		boolean synthetic = false;
		CallInfo ci = pm.getOpcodes(handle);
		synthetic = (Opcodes.ACC_SYNTHETIC & ci.getOpcodes()) == Opcodes.ACC_SYNTHETIC;
		if (synthetic) {
			return false;
		}
		
		if (PurityChecker.classIsAnonymousInner(handle.getDeclaringClass(cl))) {
			return false;
		}
		
		return pub || ((!priv) && (!prot));
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy