GenericSignatureParsingTest.java

/* *******************************************************************
 * Copyright (c) 2005 Contributors
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Andy Clement (IBM)     initial implementation
 * ******************************************************************/
package org.aspectj.apache.bcel.classfile.tests;

import java.util.ArrayList;
import java.util.List;

import org.aspectj.apache.bcel.Constants;
import org.aspectj.apache.bcel.classfile.Attribute;
import org.aspectj.apache.bcel.classfile.ClassFormatException;
import org.aspectj.apache.bcel.classfile.JavaClass;
import org.aspectj.apache.bcel.classfile.Method;
import org.aspectj.apache.bcel.classfile.Signature;
import org.aspectj.apache.bcel.classfile.Utility;

/**
 * Generics introduces more complex signature possibilities, they are no longer just
 * made up of primitives or big 'L' types. The addition of 'anglies' due to
 * parameterization and the ability to specify wildcards (possibly bounded)
 * when talking about parameterized types means we need to be much more sophisticated.
 *
 *
 * Notes:
 * Signatures are used to encode Java programming language type informaiton
 * that is not part of the JVM type system, such as generic type and method
 * declarations and parameterized types.  This kind of information is
 * needed to support reflection and debugging, and by the Java compiler.
 *
 * =============================================
 *
 * ClassTypeSignature =      LPackageSpecifier* SimpleClassTypeSignature ClassTypeSignatureSuffix*;
 *
 * PackageSpecifier =        Identifier/PackageSpecifier*
 * SimpleClassTypeSignature= Identifier TypeArguments(opt)
 * ClassTypeSignatureSuffix= .SimpleClassTypeSignature
 * TypeVariableSignature =   TIdentifier;
 * TypeArguments =           <TypeArgument+>
 * TypeArgument =            WildcardIndiciator(opt) FieldTypeSignature
 *                           *
 * WildcardIndicator =       +
 *                           -
 * ArrayTypeSignature =      [TypeSignature
 * TypeSignature =           [FieldTypeSignature
 *                           [BaseType
 *
 *                           <not sure those [ should be prefixing fts and bt>
 * Examples:
 * 	Ljava/util/List;                      ==   java.util.List
 *  Ljava/util/List<Ljava/lang/String;>;  ==   java.util.List<java.lang.String>
 *  Ljava/util/List<Ljava/lang/Double;>;  ==   java.util.List<java.lang.Double>
 *  Ljava/util/List<+Ljava/lang/Number;>; ==   java.util.List<? extends java.lang.Number>
 *  Ljava/util/List<-Ljava/lang/Number;>; ==   java.util.List<? super java.lang.Number>
 *  Ljava/util/List<*>;                   ==   java.util.List<?>
 *  Ljava/util/Map<*-Ljava/lang/Number;>; ==   java.util.Map<?,? super java.lang.Number>
 *
 * =============================================
 *
 * ClassSignature =          FormalTypeParameters(opt) SuperclassSignature SuperinterfaceSignatures*
 *
 *   optional formal type parameters then a superclass signature then a superinterface signature
 *
 * FormalTypeParameters =    <FormalTypeParameter+>
 * FormalTypeParameter  =    Identifier ClassBound InterfaceBound*
 * ClassBound =              :FieldTypeSignature(opt)
 * InterfaceBound =          :FieldTypeSignature
 *
 *   If it exists, a set of formal type parameters are contained in anglies and consist of an identifier a classbound (assumed to be
 *   object if not specified) and then an optional list of InterfaceBounds
 *
 * SuperclassSignature =     ClassTypeSignature
 * SuperinterfaceSignature = ClassTypeSignature
 * FieldTypeSignature =      ClassTypeSignature
 *                           ArrayTypeSignature
 *                           TypeVariableSignature
 *
 *
 * MethodTypeSignature =     FormalTypeParameters(opt) ( TypeSignature* ) ReturnType ThrowsSignature*
 * ReturnType =              TypeSignature
 *                           VoidDescriptor
 * ThrowsSignature =         ^ClassTypeSignature
 *                           ^TypeVariableSignature
 *
 *  Examples:
 *
 * <T::Ljava/lang/Comparable<-Ljava/lang/Number;>;>
 *
 * ClassBound not supplied, Object assumed.  Interface bound is Comparable<? super Number>
 *
 * "T:Ljava/lang/Object;:Ljava/lang/Comparable<-TT;>;","T extends java.lang.Object & java.lang.Comparable<? super T>"
 *
 */
public class GenericSignatureParsingTest extends BcelTestCase {


	/**
	 * Throw some generic format signatures at the BCEL signature
	 * parsing code and see what it does.
	 */
	public void testParsingGenericSignatures_ClassTypeSignature() {
		// trivial
		checkClassTypeSignature("Ljava/util/List;","java.util.List");

		// basics
		checkClassTypeSignature("Ljava/util/List<Ljava/lang/String;>;","java.util.List<java.lang.String>");
		checkClassTypeSignature("Ljava/util/List<Ljava/lang/Double;>;","java.util.List<java.lang.Double>");

		// madness
		checkClassTypeSignature("Ljava/util/List<+Ljava/lang/Number;>;","java.util.List<? extends java.lang.Number>");
		checkClassTypeSignature("Ljava/util/List<-Ljava/lang/Number;>;","java.util.List<? super java.lang.Number>");
		checkClassTypeSignature("Ljava/util/List<*>;",                  "java.util.List<?>");
		checkClassTypeSignature("Ljava/util/Map<*-Ljava/lang/Number;>;","java.util.Map<?,? super java.lang.Number>");

		// with type params
		checkClassTypeSignature("Ljava/util/Collection<TT;>;","java.util.Collection<T>");

		// arrays
		checkClassTypeSignature("Ljava/util/List<[Ljava/lang/String;>;","java.util.List<java.lang.String[]>");
		checkClassTypeSignature("[Ljava/util/List<Ljava/lang/String;>;","java.util.List<java.lang.String>[]");

	}


	public void testMethodTypeToSignature() {
	  checkMethodTypeToSignature("void",new String[]{"java.lang.String[]","boolean"},"([Ljava/lang/String;Z)V");
	  checkMethodTypeToSignature("void",new String[]{"java.util.List<java/lang/String>"},"(Ljava/util/List<java/lang/String>;)V");
	}

	public void testMethodSignatureToArgumentTypes() {
	  checkMethodSignatureArgumentTypes("([Ljava/lang/String;Z)V",new String[]{"java.lang.String[]","boolean"});
//	  checkMethodSignatureArgumentTypes("(Ljava/util/List<java/lang/String>;)V",new String[]{"java.util.List<java/lang/String>"});
	}

	public void testMethodSignatureReturnType() {
	  checkMethodSignatureReturnType("([Ljava/lang/String;)Z","boolean");
	}

	public void testLoadingGenerics() throws ClassNotFoundException {
		JavaClass clazz = getClassFromJar("PossibleGenericsSigs");
		// J5TODO asc fill this bit in...
	}


	// helper methods below

	// These routines call BCEL to determine if it can correctly translate from one form to the other.
	private void checkClassTypeSignature(String sig, String expected) {
		StringBuilder result = new StringBuilder();
		int p = GenericSignatureParsingTest.readClassTypeSignatureFrom(sig,0,result,false);
		assertTrue("Only swallowed "+p+" chars of this sig "+sig+" (len="+sig.length()+")",p==sig.length());
		assertTrue("Expected '"+expected+"' but got '"+result.toString()+"'",result.toString().equals(expected));
	}

	private void checkMethodTypeToSignature(String ret,String[] args,String expected) {
		String res = GenericSignatureParsingTest.methodTypeToSignature(ret,args);
		if (!res.equals(expected)) {
			fail("Should match.  Got: "+res+"  Expected:"+expected);
		}
	}

	private void checkMethodSignatureReturnType(String sig,String expected) {
		String result = GenericSignatureParsingTest.methodSignatureReturnType(sig,false);
		if (!result.equals(expected)) {
			fail("Should match.  Got: "+result+"  Expected:"+expected);
		}
	}

	private void checkMethodSignatureArgumentTypes(String in,String[] expected) {
		String[] result = GenericSignatureParsingTest.methodSignatureArgumentTypes(in,false);
		if (result.length!=expected.length) {
			fail("Expected "+expected.length+" entries to be returned but only got "+result.length);
		}
		for (int i = 0; i < expected.length; i++) {
			String string = result[i];
			if (!string.equals(expected[i]))
				fail("Argument: "+i+" should have been "+expected[i]+" but was "+string);
		}
	}

	public Signature getSignatureAttribute(JavaClass clazz,String name) {
		Method m = getMethod(clazz,name);
		Attribute[] as = m.getAttributes();
		for (Attribute attribute : as) {
			if (attribute.getName().equals("Signature")) {
				return (Signature) attribute;
			}
		}
		return null;
	}


	/**
	   * Takes a string and consumes a single complete signature from it, returning
	   * how many chars it consumed.  The chopit flag indicates whether to shorten
	   * type references ( java/lang/String => String )
	   *
	   * FIXME asc this should also create some kind of object you can query for information about whether its parameterized, what the bounds are, etc...
	   */
	  public static final int readClassTypeSignatureFrom(String signature, int posn, StringBuilder result, boolean chopit) {
		    int idx = posn;
		    try {
		      switch (signature.charAt(idx)) {
		        case 'B' : result.append("byte");   return 1;
		        case 'C' : result.append("char");   return 1;
		        case 'D' : result.append("double"); return 1;
		        case 'F' : result.append("float");  return 1;
		        case 'I' : result.append("int");    return 1;
		        case 'J' : result.append("long");   return 1;
			    case 'S' : result.append("short");  return 1;
			    case 'Z' : result.append("boolean");return 1;
		        case 'V' : result.append("void");   return 1;


				//FIXME ASC Need a state machine to check we are parsing the right stuff here !
		        case 'T' :
					idx++;
					int nextSemiIdx = signature.indexOf(';',idx);
					result.append(signature.substring(idx,nextSemiIdx));
					return nextSemiIdx+1-posn;

		        case '+' :
					result.append("? extends ");
					return readClassTypeSignatureFrom(signature,idx+1,result,chopit)+1;

				case '-' :
					result.append("? super ");
					return readClassTypeSignatureFrom(signature,idx+1,result,chopit)+1;

				case '*' :
					result.append("?");
					return 1;

				case 'L' : // Full class name
			      boolean parameterized = false;
		      	  int idxSemicolon = signature.indexOf(';',idx); // Look for closing ';' or '<'
				  int idxAngly     = signature.indexOf('<',idx);
				  int endOfSig = idxSemicolon;
				  if ((idxAngly!=-1) && idxAngly<endOfSig) { endOfSig = idxAngly; parameterized = true; }

				  String p = signature.substring(idx+1,endOfSig);
				  String t = Utility.compactClassName(p,chopit);

				  result.append(t);
				  idx=endOfSig;
				  // we might have finished now, depending on whether this is a parameterized type...
				  if (parameterized) {
					  idx++;
					  result.append("<");
					  while (signature.charAt(idx)!='>') {
						  idx+=readClassTypeSignatureFrom(signature,idx,result,chopit);
						  if (signature.charAt(idx)!='>') result.append(",");
					  }
					  result.append(">");idx++;
				  }
				  if (signature.charAt(idx)!=';') throw new RuntimeException("Did not find ';' at end of signature, found "+signature.charAt(idx));
				  idx++;
				  return idx-posn;


		      case '[' :  // Array declaration
				  int dim = 0;
				  while (signature.charAt(idx)=='[') {dim++;idx++;}
				  idx+=readClassTypeSignatureFrom(signature,idx,result,chopit);
				  while (dim>0) {result.append("[]");dim--;}
				  return idx-posn;

		      default  : throw new ClassFormatException("Invalid signature: `" +
							    signature + "'");
		      }
		    } catch(StringIndexOutOfBoundsException e) { // Should never occur
		      throw new ClassFormatException("Invalid signature: " + e + ":" + signature);
		    }
		  }


	public static final String readClassTypeSignatureFrom(String signature) {
	    StringBuilder sb = new StringBuilder();
		GenericSignatureParsingTest.readClassTypeSignatureFrom(signature,0,sb,false);
		return sb.toString();
	  }


	public static int countBrackets(String brackets) {
	    char[]  chars = brackets.toCharArray();
	    int     count = 0;
	    boolean open  = false;

		for (char aChar : chars) {
			switch (aChar) {
				case '[':
					if (open) throw new RuntimeException("Illegally nested brackets:" + brackets);
					open = true;
					break;

				case ']':
					if (!open) throw new RuntimeException("Illegally nested brackets:" + brackets);
					open = false;
					count++;
					break;

				default:
			}
		}

	    if (open) throw new RuntimeException("Illegally nested brackets:" + brackets);

	    return count;
	  }


	/**
	   * Parse Java type such as "char", or "java.lang.String[]" and return the
	   * signature in byte code format, e.g. "C" or "[Ljava/lang/String;" respectively.
	   *
	   * @param  type Java type
	   * @return byte code signature
	   */
	  public static String getSignature(String type) {
	    StringBuilder buf       = new StringBuilder();
	    char[]       chars      = type.toCharArray();
	    boolean      char_found = false, delim = false;
	    int          index      = -1;

	    loop:
	      for (int i=0; i < chars.length; i++) {
	        switch (chars[i]) {
	          case ' ': case '\t': case '\n': case '\r': case '\f':
		        if (char_found) delim = true;
		        break;

	          case '[':
		        if (!char_found) throw new RuntimeException("Illegal type: " + type);
		        index = i;
		        break loop;

	          default:
		        char_found = true;
		        if (!delim) buf.append(chars[i]);
	        }
	      }

	    int brackets = 0;

	    if(index > 0) brackets = GenericSignatureParsingTest.countBrackets(type.substring(index));

	    type = buf.toString();
	    buf.setLength(0);

	    for (int i=0; i < brackets; i++) buf.append('[');

	    boolean found = false;

	    for(int i=Constants.T_BOOLEAN; (i <= Constants.T_VOID) && !found; i++) {
	      if (Constants.TYPE_NAMES[i].equals(type)) {
		    found = true;
		    buf.append(Constants.SHORT_TYPE_NAMES[i]);
	      }
	    }

	    // Class name
	    if (!found) buf.append('L' + type.replace('.', '/') + ';');

	    return buf.toString();
	  }


	/**
	   * For some method signature (class file format) like '([Ljava/lang/String;)Z' this returns
	   * the string representing the return type its 'normal' form, e.g. 'boolean'
	   *
	   * @param  signature    Method signature
	   * @param  chopit       Shorten class names
	   * @return return type of method
	   */
	  public static final String methodSignatureReturnType(String signature,boolean chopit) throws ClassFormatException {
	    int    index;
	    String type;
	    try {
	      // Read return type after `)'
	      index = signature.lastIndexOf(')') + 1;
	      type = Utility.signatureToString(signature.substring(index), chopit);
	    } catch (StringIndexOutOfBoundsException e) {
	      throw new ClassFormatException("Invalid method signature: " + signature);
	    }
	    return type;
	  }


	/**
	   * For some method signature (class file format) like '([Ljava/lang/String;)Z' this returns
	   * the string representing the return type its 'normal' form, e.g. 'boolean'
	   *
	   * @param  signature    Method signature
	   * @return return type of method
	   * @throws  ClassFormatException
	   */
	  public static final String methodSignatureReturnType(String signature) throws ClassFormatException {
	    return GenericSignatureParsingTest.methodSignatureReturnType(signature, true);
	  }


	/**
	   * For some method signature (class file format) like '([Ljava/lang/String;Z)V' this returns an array
	   * of strings representing the arguments in their 'normal' form, e.g. '{java.lang.String[],boolean}'
	   *
	   * @param  signature    Method signature
	   * @param     chopit    Shorten class names
	   * @return              Array of argument types
	   */
	  public static final String[] methodSignatureArgumentTypes(String signature,boolean chopit) throws ClassFormatException {
	    List<String> vec = new ArrayList<>();
	    int       index;
	    String[]  types;

	    try { // Read all declarations between for `(' and `)'
	      if (signature.charAt(0) != '(')
		    throw new ClassFormatException("Invalid method signature: " + signature);

	      index = 1; // current string position

	      while(signature.charAt(index) != ')') {
	      	Utility.ResultHolder rh = Utility.signatureToStringInternal(signature.substring(index),chopit);
		    vec.add(rh.getResult());
		    index += rh.getConsumedChars();
	      }
	    } catch(StringIndexOutOfBoundsException e) {
	      throw new ClassFormatException("Invalid method signature: " + signature);
	    }

	    types = new String[vec.size()];
	    vec.toArray(types);
	    return types;
	  }


	/**
	   * Converts string containing the method return and argument types
	   * to a byte code method signature.
	   *
	   * @param  returnType Return type of method (e.g. "char" or "java.lang.String[]")
	   * @param  methodArgs Types of method arguments
	   * @return Byte code representation of method signature
	   */
	  public final static String methodTypeToSignature(String returnType, String[] methodArgs) throws ClassFormatException {

	    StringBuilder buf = new StringBuilder("(");

	    if (methodArgs != null) {
			for (String methodArg : methodArgs) {
				String str = GenericSignatureParsingTest.getSignature(methodArg);

				if (str.equals("V")) // void can't be a method argument
					throw new ClassFormatException("Invalid type: " + methodArg);

				buf.append(str);
			}
	    }

	    buf.append(")" + GenericSignatureParsingTest.getSignature(returnType));

	    return buf.toString();
	  }

}