Bug 90215 - [1.5] Enums implementing generic interfaces can compile incorrectly.
Summary: [1.5] Enums implementing generic interfaces can compile incorrectly.
Status: VERIFIED FIXED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: Core (show other bugs)
Version: 3.1   Edit
Hardware: PC Linux
: P3 normal (vote)
Target Milestone: 3.1 RC1   Edit
Assignee: Philipe Mulet CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2005-04-04 15:30 EDT by James Stangler CLA
Modified: 2005-05-27 10:26 EDT (History)
1 user (show)

See Also:


Attachments
Contains the working and failing versions of the java and class files (29.32 KB, application/octet-stream)
2005-04-04 15:33 EDT, James Stangler CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description James Stangler CLA 2005-04-04 15:30:41 EDT
I tried to reduce this to a simple reproducible example but I was unable to do
so because the same source file would sometimes compile and work correctly and
sometimes compile incorrectly and I couldn't determine what conditions caused
it.  When I tried to simplify my problem code I couldn't reproduce the problem.
 Hopefully you'll be able to do something with what I could submit.

First, the problem relates to the code generated for an Enum that implements an
interface one method having a generic return type and implementing the method in
each enum value instead of one implementing method for the whole enum class.  In
this code, there is a class called "Shipment".  It has an field called
workflowType whose type is an enum class called WorkflowType.  The enum class
implements an interface ValuedEnum<String>.  The interface has a method called
"getInternalValue()" whose return type is the generic type used for the
interface, in this case String.  There is a class called
TestEnumImplementGenerics which instantiates the Shipment class, sets the
workflowType to the Buyer enum value, invokes the getInternalValue() method and
stores the result in a String (forcing the cast from Object).

When I first ran the TestEnumImplementGenerics class, I got the following error:
Value 1
Exception in thread "main" java.lang.AbstractMethodError:
com.aex.repair.ship.data.Shipment$WorkflowType.getInternalValue()Ljava/lang/Object;
	at
com.aex.repair.ship.data.TestEnumImplementGenerics.main(TestEnumImplementGenerics.java:25)

I went in the Shipment class and added "public abstract String
getInternalValue();" to the WorkflowType enum.  When I ran the test, the output was:
Value 1
BUYER

I then commented out the line I just added.  I still received the same output:
Value 1
BUYER

I then cleaned the project (no code changes) - just a recompilation.  Now I get
the error again:
Value 1
Exception in thread "main" java.lang.AbstractMethodError:
com.aex.repair.ship.data.Shipment$WorkflowType.getInternalValue()Ljava/lang/Object;
	at
com.aex.repair.ship.data.TestEnumImplementGenerics.main(TestEnumImplementGenerics.java:25)


This may also be a problem with the incremental compiler.

I'm took a copy of the java and class files when it worked and when it failed. 
I'll attach those.  Maybe someone can look at the results and figure out how to
reproduce it and what's going wrong.
Comment 1 James Stangler CLA 2005-04-04 15:33:13 EDT
Created attachment 19521 [details]
Contains the working and failing versions of the java and class files

This tar/gz file contains a directory called "generics".  Inside is a directory
called "fails" and one called "passes".  The only difference between them is
the generated class files for one of the enum types.

bash-2.05b$ diff -r /tmp/generics/fails /tmp/generics/passes
Files /tmp/generics/fails/com/aex/repair/ship/data/Shipment$1.class and
/tmp/generics/passes/com/aex/repair/ship/data/Shipment$1.class differ
Files /tmp/generics/fails/com/aex/repair/ship/data/Shipment$2.class and
/tmp/generics/passes/com/aex/repair/ship/data/Shipment$2.class differ
Comment 2 Olivier Thomann CLA 2005-04-04 17:06:26 EDT
What build are you using?
Comment 3 James Stangler CLA 2005-04-04 17:29:58 EDT
Sorry.  3.1M6
Comment 4 James Stangler CLA 2005-04-21 10:46:02 EDT
Ok, I finally have a small test case which illustrates the problem on my
machine.  I'm using 3.1M6 on Gentoo Linux.

1.  First you need to create two classes ClassWithBadEnum.java and
Placeholder.java in package "bad":

ClassWithBadEnum.java:
package bad;

public class ClassWithBadEnum {
	public interface EnumInterface<T extends Object> {
	    public T getMethod();
	}

	public enum EnumClass implements EnumInterface<String> {
		ENUM1 { public String getMethod() { return "ENUM1";} },
		ENUM2 { public String getMethod() { return "ENUM2";} };
	}
	private EnumClass enumVar;
	public EnumClass getEnumVar() {
		return enumVar;
	}
	public void setEnumVar(EnumClass enumVar) {
		this.enumVar = enumVar;
	}

	public static void main(String... argv) {
		int a = 1;
		ClassWithBadEnum badEnum = new ClassWithBadEnum();
		badEnum.setEnumVar(ClassWithBadEnum.EnumClass.ENUM1);
		// Should fail if bug manifests itself because there will be two
getInternalValue() methods
		// one returning an Object instead of a String
		System.out.println("Should fail with a java.lang.AbstractMethodError if the
bug manifests itself");
		String s3 = badEnum.getEnumVar().getMethod();
		System.out.println("Didn't fail");
		System.out.println(s3);
	}
}

Placeholder.java:
package bad;

public class Placeholder {
    public static void main(String... argv) {
        ClassWithBadEnum badEnum = new ClassWithBadEnum();
		 badEnum.setEnumVar(ClassWithBadEnum.EnumClass.ENUM1);
	}
}


Even though the Placeholder class doesn't do anything, I couldn't get the bug to
manifest itself without having another class referencing the problem class.

2.  Run class ClassWithBadEnum and get the following output:

Should fail with a java.lang.AbstractMethodError if the bug manifests itself
Didn't fail
ENUM1


3.  Clean project using Project>Clean...

4.  Run class ClassWithBadEnum again with the following output:

Should fail with a java.lang.AbstractMethodError if the bug manifests itself
Exception in thread "main" java.lang.AbstractMethodError:
bad.ClassWithBadEnum$EnumClass.getMethod()Ljava/lang/Object;
	at bad.ClassWithBadEnum.main(ClassWithBadEnum.java:26)


5.  Make any change (such as changing the value of variable "a" from 1 to 2 in
ClassWithBadEnum).

6.  Run class ClassWithBadEnum and get the following output:

Should fail with a java.lang.AbstractMethodError if the bug manifests itself
Didn't fail
ENUM1
Comment 5 Abdullah Jibaly CLA 2005-05-05 13:58:54 EDT
I'm getting the same error running Eclipse 3.1M6 on Windows XP Pro testing the
case described by Jim: the bytecode generated by cleaning the project fails
whereas the incrementally generated class file does not.
Comment 6 Philipe Mulet CLA 2005-05-17 16:52:12 EDT
Reproduced
Comment 7 Philipe Mulet CLA 2005-05-17 17:21:51 EDT
Problem is that in clean build scenario, first enum constant ENUM1 generated
class: ClassWithBadEnum$1 is missing definition for method: #getMethod(); hence
the AbstractMethodError.

Comment 8 Philipe Mulet CLA 2005-05-17 17:56:26 EDT
Actually problem is that an offending extra bridge method got inserted on class
for ENUM1.
Comment 9 Philipe Mulet CLA 2005-05-17 17:57:05 EDT
GOOD 

  // Method descriptor #18 ()Ljava/lang/String;
  // Stack: 1, Locals: 1
  public String getMethod();
    0  ldc <String "ENUM1"> [20]
    2  areturn
      Line numbers:
        [pc: 0, line: 9]
      Local variable table:
        [pc: 0, pc: 3] local: this index: 0 type: Lp/ClassWithBadEnum$1;

BAD  
  // Method descriptor #21 ()Ljava/lang/Object;
  // Stack: 1, Locals: 1
  public bridge synthetic Object getMethod();
    0  aload_0 [local_0]
    1  invokevirtual p/ClassWithBadEnum$1.getMethod()Ljava/lang/String; [23]
    4  areturn
      Line numbers:
        [pc: 0, line: 1]
Comment 10 Philipe Mulet CLA 2005-05-17 18:21:58 EDT
Ignore comments from comment#7 to comment#9.

Problem is that somehow the bridge methods did not get generated in clean build
situation, but fine incrementally. Also trying to recreate the problem in batch
mode did not reproduce it either.
Comment 11 Philipe Mulet CLA 2005-05-17 19:03:33 EDT
Issue seems located in method verifier.
Put a breakpoint in MethodVerifier#checkMethods()

When recreating (in runtime workbench, clean build), first hit of the breakpoint
is during remote resolution of ENUM1 field (when accessed from Placeholder).

void checkMethods() {
	boolean mustImplementAbstractMethods = mustImplementAbstractMethods();
	boolean skipInheritedMethods = mustImplementAbstractMethods &&
canSkipInheritedMethods(); // have a single concrete superclass so only check
overridden methods

mustImplementAbstractMethods == false
skipInheritedMethods == false

Then later on, we hit the breakpoint again when processing the same local type;
however this time around

mustImplementAbstractMethods == false
skipInheritedMethods == true !?!?

Q1. why do we verify it twice ?
Q2. why different setup then ?
Comment 12 Philipe Mulet CLA 2005-05-17 19:17:49 EDT
Before debugging, close all editors in runtime workspace to avoid confusion.
Actual data is then:

- hit on #checkMethods() for ENUM1 occurs through Placeholder forward ref.
Then:
mustImplementAbstractMethods == true
skipInheritedMethods == true

Then later on, we hit the breakpoint again when processing ENUM2 in context of
its declaring class (no reprocessing of ENUM1). However then:

mustImplementAbstractMethods == true
skipInheritedMethods == false  ?!?

This is the scenario in which then ENUM2 gets a bridge method, whereas ENUM1 did
not get any.

Suspecting it has to do with abstractness of superclass EnumClass
Comment 13 Philipe Mulet CLA 2005-05-17 19:25:43 EDT
Indeed, in between the 2 hits, we enter the following code which toggles
abstractness of enum superclass.

boolean mustImplementAbstractMethod(ReferenceBinding declaringClass) {
	if (!this.type.isEnum())
		return super.mustImplementAbstractMethod(declaringClass);
	if (this.type.isAnonymousType())
		return true; // body of enum constant must implement any inherited abstract
methods
	if (this.type.isAbstract())
		return false; // is an enum that has since been tagged as abstract by the code
below

	// enum type needs to implement abstract methods if one of its constants does
not supply a body
	TypeDeclaration typeDeclaration = this.type.scope.referenceContext;
	FieldDeclaration[] fields = typeDeclaration.fields;
	int length = typeDeclaration.fields == null ? 0 : typeDeclaration.fields.length;
	if (length == 0) return true; // has no constants so must implement the method
itself
	for (int i = 0; i < length; i++) {
		FieldDeclaration fieldDecl = fields[i];
		if (fieldDecl.getKind() == AbstractVariableDeclaration.ENUM_CONSTANT)
			if (!(fieldDecl.initialization instanceof QualifiedAllocationExpression))
				return true;
	}

	// tag this enum as abstract since an abstract method must be implemented AND
all enum constants define an anonymous body
	// as a result, each of its anonymous constants will see it as abstract and
must implement each inherited abstract method
	this.type.modifiers |= IConstants.AccAbstract;
	return false;
}
Comment 14 Philipe Mulet CLA 2005-05-17 19:45:52 EDT
I have a fix. I moved the abstract bit setting to
ClassScope#checkAndSetModifiers, where it should have lived initially.

Suspect a proper testcase needs to written as a builder test, since
ClassWithBadEnum needs to be requested as source, and not initially on the
command line as when using batch test.
Comment 15 Philipe Mulet CLA 2005-05-18 05:13:30 EDT
Added EnumTest#test104. Important point is to have a forward reference from
Placeholder to ENUM1.

Fixed
Comment 16 David Audel CLA 2005-05-27 10:26:37 EDT
Verified in i20050527-0010