Skip to content

Fix unchecked conversion not failed compilation#4961

Open
chrisrueger wants to merge 1 commit into
eclipse-jdt:masterfrom
chrisrueger:fix-gh4957
Open

Fix unchecked conversion not failed compilation#4961
chrisrueger wants to merge 1 commit into
eclipse-jdt:masterfrom
chrisrueger:fix-gh4957

Conversation

@chrisrueger

@chrisrueger chrisrueger commented Mar 20, 2026

Copy link
Copy Markdown

Closes #4957

What it does

If unchecked conversion was necessary for the method to be applicable, then the invocation type's parameter types are obtained by applying the substitution [P1:=T1, ..., Pp:=Tp] to the parameter types of the method's type, and the invocation type's return type and thrown types are given by the erasure of the return type and thrown types of the method's type.

  • Adds regression test testGH4957 reproducing the Comparator.comparing + raw Comparable scenario With this fix testGH4957 passes.

How to test

  • run org.eclipse.jdt.core.tests.compiler.regression.GenericsRegressionTest_9.testGH4957() without the fix in org.eclipse.jdt.internal.compiler.lookup.ParameterizedGenericMethodBinding.computeCompatibleMethod(MethodBinding, TypeBinding[], Scope, InvocationSite)

or just use e.g. Eclipse 2025-03 (or even 2026-06 (4.40) Build id: I20260319-1800 master) and paste the the following class:

import java.util.Comparator;
import java.util.Map;

public class Snippet {

    public static void main(String[] args) {

        Comparator<Map<String, Object>> eventComparator = Comparator
            .<Map<String, Object>, Comparable>comparing(e -> (Comparable) e.get("event_date"),
                Comparator.nullsLast(Comparator.naturalOrder()))
            .thenComparing(e -> (String) e.get("event_type"));
    }

}

Expected Result: Compilation should fail in Eclipse
Actual result: Compiliation succeeds.

But in javacc compilation fails.

With this PR compilation fails also in Eclipse.

But note: Eclipse seems to error with a different message than javac. Not sure this is a deeper problem. But at least it fails at the same place.

ECJ fails with:

"5. ERROR in Snippet.java (at line 11)\n" +
			"	.thenComparing(e -> (String) e.get(\"event_type\"));\n" +
			"	                               ^^^\n" +
			"The method get(String) is undefined for the type Object\n" +

while javac failed with:

Snippet.java:13: error: cannot find symbol
            .thenComparing(e -> (String) e.get("event_type"));
                                          ^
  symbol:   method get(String)
  location: variable e of type Object
Snippet.java:13: warning: [unchecked] unchecked conversion
            .thenComparing(e -> (String) e.get("event_type"));
                          ^
  required: Comparator<Map<String,Object>>
  found:    Comparator
1 error
5 warnings
error: compilation failed

Disclaimer:

I used AI to help me understand the issue and spec and find the places in the code. I have manually tested and debugged via latest JDT Dev environment via Oomph and ran my test in Eclipse.
This is my first step in the compiler space. Feel free to reject it or just use it as a base for a better fix if needed.

Author checklist

@venkateshkona3456-oss

Copy link
Copy Markdown

Hello,
I would like to work on this issue. Is it available?

Thank you!

@stephan-herrmann stephan-herrmann self-requested a review May 31, 2026 22:02
@stephan-herrmann

Copy link
Copy Markdown
Contributor

Thanks for submitting.

A few preliminary remarks:

Detect when any explicit type argument is a raw type and then fail compilation (to achieve same compilation error as javac)

Of course causality is much more indirect: detecting raw types in that position should not (and does not) directly cause compilation to fail. Did you understand how the change triggers the compile error in this particular case? Perhaps adding another similar test which passes helps (now and for posterity) to understand the actual effect of the change?

This seems to be related to https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.12.2.6

It would help to be more specific, stating which part of that section is not correctly implemented in ecj.

Actually my own first reaction was: isn't all this already handled in type inference? (no, it is not).

Expected Result: Compilation should fail in Eclipse
Actual result: Compiliation succeeds.

In many cases of type checking it is not obvious, which way a compiler should behave. Comparing ecj to javac gives a hint but not a proof. Ideally, when a compiler accepts a program wrongly, this can be proven by crafting a program that is accepted by the compiler but throws an unexpected exception at run time. Do you think this kind of proof might be possible for this example? Is anything obviously wrong about the byte code that ecj generates?

But note: Eclipse seems to error with a different message than javac.

The exact error messages are not standardized. If you look closer, do both error messages tell different stories or the same story just with different words?

@stephan-herrmann stephan-herrmann added compiler Eclipse Java Compiler (ecj) related issues javac ecj not compatible with javac labels May 31, 2026
@chrisrueger

Copy link
Copy Markdown
Author

Thanks @stephan-herrmann I need to wrap my around this again. Will take some time, but do my best.

Detect when any explicit type argument is a raw type
Adds regression test testGH4957 reproducing the Comparator.comparing + raw Comparable scenario (issue eclipse-jdt#4957) and asserting the expected warnings/errors.
With this fix testGH4957 passes.
@chrisrueger

chrisrueger commented Jun 7, 2026

Copy link
Copy Markdown
Author

I did had a look at it and tried to understand your comment.

This seems to be related to https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.12.2.6

It would help to be more specific, stating which part of that section is not correctly implemented in ecj.

Actually my own first reaction was: isn't all this already handled in type inference? (no, it is not).

I think I meant the following section of the spec:

If unchecked conversion was necessary for the method to be applicable, then the invocation type's parameter types are obtained by applying the substitution [P1:=T1, ..., Pp:=Tp] to the parameter types of the method's type, and the invocation type's return type and thrown types are given by the erasure of the return type and thrown types of the method's type.

Did you understand how the change triggers the compile error in this particular case?

maybe....maybe not...
I need to be honest here. As said, this is my first step this compiler space and I using AI to help me understand it.

My interpretation is this:
After the change:

  • Comparator.<Map<String, Object>, Comparable>comparing(e -> (Comparable) e.get("event_date")
  • by JLS 15.12.2.6 the invocation return type is erased: meaning result type becomes raw Comparator, not Comparator<Map<String,Object>>
  • .thenComparing(...) is invoked on raw Comparator
  • lambda parameter e is treated as Object
  • and e.get(String) does not exist on object
  • Boom, error.

Before the change the Comparator<Map<String,Object>> was **not** erased and carried into .thenComparing(...) so in ECJ thenComparing(...) thought: " hey...e is Map<String,Object>> on which I can call e.get(String))

But javac seems to be stricter.
Something like that.

Ideally, when a compiler accepts a program wrongly, this can be proven by crafting a program that is accepted by the compiler but throws an unexpected exception at run time.

I tried to construct a runtime-failing example.
The snippet below is accepted by ECJ and fails with ClassCastException at runtime, but still fails compilation in javac.

import java.util.Comparator;
import java.util.Map;

public class Snippet2 {
    public static void main(String[] args) {

    	Comparator raw = Comparator
			        .<Map<String, Object>, Comparable>comparing(
			            e -> (Comparable) e.get("event_date"),
			            Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(
                e -> (String)  e.get("event_type"));

            System.out.println(raw.compare("not a map", "also not a map"));
    }
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.util.Map (java.lang.String and java.util.Map are in module java.base of loader 'bootstrap')
	at java.base/java.util.Comparator.lambda$comparing$ea9a8b3a$1(Comparator.java:440)
	at java.base/java.util.Comparator.lambda$thenComparing$36697e65$1(Comparator.java:220)
	at org.eclipse.jdt.core.tests.compiler.regression.Snippet2.main(Snippet2.java:18)

Javac gives:

java -Duser.language=en /Users/christophrueger/jdt-master/git/eclipse.jdt.core/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/Snippet2.java

            e -> (String)  e.get("event_type"));
                            ^
  symbol:   method get(String)
  location: variable e of type Object
/Users/christophrueger/jdt-master/git/eclipse.jdt.core/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/Snippet2.java:15: warning: [unchecked] unchecked call to compare(T,T) as a member of the raw type Comparator
        System.out.println(raw.compare("not a map", "also not a map"));
                                      ^
  where T is a type-variable:
    T extends Object declared in interface Comparator
1 error
5 warnings
error: compilation failed

I am not sure this is what you were looking for.

This is not a good proof of the original bug in isolation, because the runtime failure also depends on explicit raw usage at the call site. But it does show that the unchecked/raw behavior in this area can lead to concrete runtime problems.

What do you think is a valid next step. Would you like to take over and just use my beginner thoughts as input?

@stephan-herrmann

Copy link
Copy Markdown
Contributor

Thanks for your response @chrisrueger

Let me put the cards on the table:

What I've seen so far looks very good. Still we need to build trust that the fix really fixes the core problem, and does so without causing grief elsewhere.

Several ways how this trust can be established:

  1. You explain in clear words, where the current implementation is wrong, and what exactly is wrong about it. Then argue why the fix resolves exactly that wrong (and nothing else).
  2. Or: Failing (2) I would have to repeat much of the background analysis to come to that explanation on my own.
  3. No, I will not trust AI. I.e., no matter what "smart" things AI "suggested", some human in the loop must own the reasoning.

Now, let's see where we stand with your response:

I think I meant the following section of the spec:

If unchecked conversion was necessary for the method to be applicable, then the invocation type's parameter types are obtained by applying the substitution [P1:=T1, ..., Pp:=Tp] to the parameter types of the method's type, and the invocation type's return type and thrown types are given by the erasure of the return type and thrown types of the method's type.

The weak part here is "I think I meant", the rest is good.

As said, this is my first step this compiler space

You chose no simple code area, and hence no need to be shy if you are not 100% certain. Let's work it out together.

My interpretation is this:
After the change: ...

All that follows looks good. I'll still verify it in the debugger, but so far no complaints.

I tried to construct a runtime-failing example.

This was a tremendous challenge, actually (I had briefly tried and failed).

The snippet below is accepted by ECJ and fails with ClassCastException at runtime, but still fails compilation in javac.

The effect looks pretty similar to what I was looking for, but here the ClassCastException happens "before" the location where compilers disagree. See:

import java.util.Comparator;
import java.util.Map;

public class Snippet3 {
    public static void main(String[] args) {

    	Comparator raw = Comparator
			        .<Map<String, Integer>, Comparable>comparing(
			            e ->  e.get("event_date"));

            System.out.println(raw.compare("not a map", "also not a map"));
    }
}

This is accepted by both compilers and still throws the exact CCE you also observed. Who is to blame? Right, any use of raw types can produce CCE in location where the user didn't even write a cast.

So, the example did not succeed to distinguish between correct and incorrect compilation, which only means, we should probably focus on evidence by reasoning, rather then evidence by a witness.

What do you think is a valid next step?

I'll take a closer look at the change both in the debugger and in relation to JLS. From there I'll either come back with more questions / requests, or I may find all to be in good shape and merge it right away. Stay tuned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler Eclipse Java Compiler (ecj) related issues javac ecj not compatible with javac

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eclipse 2026-03 compiles fine but Java does not: unchecked conversion

3 participants