Skip to content

SI-9111 / SI-10107 resolution of static references in Java code #5606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from

Conversation

lrytz
Copy link
Member

@lrytz lrytz commented Dec 19, 2016

Scala puts static members of Java classes into the companion
object symbol. When parsing Java code, references to static
members need to be looked up in these companions.

In 2.11.x, a synthetic import A._ was added to Java classes
having static members, which lead to some bugs (SI-9111). The
scheme was changed in 4e822d7 (2.12.x only): when a lookup
of T / A.T fails, the typer searches in the companion of the
current class.

This fixes some issues, but not all

  • In Java, A.T can refer to a static member defined in a
    parent of A
  • If a static A.T exists, but a parent of A defines a
    non-static member class T, the non-static one would be
    chosen (wrongly)

This commit pushes the logic to consider companion objects into
FindMembers and makes sure that for ever class in the base type
sequence, the companion is considered before moving to the next
base type.

This version changes the mode for mixed compilation, which used to be

  1. scalac *.java *.scala -d o
  2. javac  *.java         -d o -cp o
  3. scalac        *.scala -d o -cp o

Now the third step is skipped. This required some adjustments to existing
tests.

  - t7014 is split in two groups, the fix is for separate compilation.
  - t7582 is also split. It tests inliner warnings when inling code that accesses
    Java-defined package-private code. Inlining from Java only works in separate
    compilation (no bytecode available in mixed compilation).
  - Java compiler warnings of "run" tests were not reported in the old scheme,
    now they are. Deprecation / unchecked warnings were removed from t6240, t8786,
    varargs.
  - t4788 required a .check file update to pass, which hints at a bug. I will
    re-open SI-4788 and investigate later.
The logic to resolve static nested classes changed 4e822d7, so the
description in the bug report (wrong imports) is no longer accurate.
The first example in the ticket is fixed, but not the second.

This commit adds tests for the status quo.

Removes a method that's no longer used since 4e822d7.
@scala-jenkins scala-jenkins added this to the 2.12.2 milestone Dec 19, 2016
@lrytz lrytz changed the title SI-10107 / SI-9111 WIP SI-9111 / SI-10107 resolution of static references in Java code Dec 20, 2016
@lrytz lrytz requested a review from adriaanm December 20, 2016 12:25
Copy link
Contributor

@adriaanm adriaanm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I have a few small suggestions.

val searchCompanions = isPlainClassInJavaUnit(pre.typeSymbol)
val sym = pre.findMember(name, excludedFlags = Flags.BridgeFlags, requiredFlags = 0, stableOnly = false, searchCompanions).filter(qualifies)
if (searchCompanions && sym.owner.isModuleClass)
pre = sym.owner.typeOfThis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much neater than companionSymbolOf!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we ever get here while completing a lazy type for sym.owner? Maybe that's not possible because of the Java-specific code path here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sym is a member, and sym.owner is a module class. I think when completing a lazy type of a class, its members' types are not completed.

@@ -984,6 +985,10 @@ trait Contexts { self: Analyzer =>

def isNameInScope(name: Name) = lookupSymbol(name, _ => true).isSuccess

def isPlainClassInJavaUnit(sym: Symbol): Boolean =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible/make sense to get the unit from sym, so that we can encapsulate this logic in findMember instead of computing searchCompanions at its call sites?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many uses of findMember other than the ones updated in this PR. This PR only searches in companions when type-checking Idents or Selects while type-checking a Java source file. For example, given

public class A { public static class T }
public class B extends A

A reference B.T resolves to A.T when it appears inside a Java source file, but should (?) give an error when inside a Scala source file. So we need to check context.unit.isJava, not only sym.isJavaDefined.

There's no compilationUnit on symbols, only sourceFile, but that's says where a symbol is defined. The context.unit.isJava check is about where the symbol is referenced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

class HasStatic { static void foo() {} }
class Client { void test() { ScalaSubclass.foo(); } }
class ScalaSubclass extends HasStatic

This shows that a Java client can access a Java static selected from a Scala subclass of the owner of the static member.

I think we should drop the check for sym.isJavaDefined here, and instead only use this to filter out companions in FindMember.

Copy link
Member Author

@lrytz lrytz Dec 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your point of moving the sym.isJavaDefined check into FindMember. However, your example is not going to work, and it will be very hard to support it:

// A.java
public class A {
  public static class C {
    public static class I { }
  }
  public static class E extends D.I { }
}
// Test.scala
class D extends A.C
object Test {
  new A.E()
}

In mixed compilation:

A.java:5: error: not found: value D
  public static class E extends D.I { }
                                ^
one error found

Copy link
Member Author

@lrytz lrytz Dec 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works if D has a companion object. It also works in separate compilation.

Thinking more about it, we should support D.I in a Java source even if D is defined in Scala and not a value. This would be very similar as the special case being implemented in this PR (allow A.T if A is a java class).

Copy link
Member Author

@lrytz lrytz Dec 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it a little trickier:

// Java
public class A {
  public static class I { }
}

// Scala
class B {
  // class I  // (1)
}
class C extends A
object C extends B {
  // class I  // (2)
}

What is C.I in a Java source? In separate compilation (mixed compilation should behave the same):

  • With 1 and 2 commented, C.I (in a Java source) resolves to A$I
  • Un-commenting 1 doesn't change anything: class B is not a parent of the class C - it's a parent of the module class C$. If a Java source references C.I, the Java compiler doesn't look in C$.
  • Un-commenting 2 triggers some compat-mode in the InnerClass attribute and marks I as being a static member class of C (not of C$), so a C.I reference in Java resolves to C$I.

So when we are in a Java compilation unit and look for a member of a module class C we should

  • first check the C module class (but not any of its parents)
  • then the C class and its parents, and also all parents module classes

If we also start type checking Java terms (in the future) things would get more complicated. Right now, in findMember, we don't know whether we're looking for a static or instance member. A type selection Outer.Inner, it can resolve to a static or non-static inner class, the same syntax is used for both. For terms, A.foo should not return an instance method.

Related older fix I wasn't aware of: scala/bug#3120. When type-checking a selection A.foo fails in a Java compilation unit, a synthetic SelectFromTypeTree tree will be type-checked as second try.

// For Java compilation units, a `T` or `A.T` can refer to a static inner class `T`. Static inner
// classes are placed in the companion object. Under `searchCompanions`, we always check the companion
// before proceeding to the next base class.
if (searchCompanions) bcs.flatMap(bc => List(bc, bc.companionModule.moduleClass))
Copy link
Contributor

@adriaanm adriaanm Dec 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some more details from your commit message here? Maybe some variation of:

In Java, static members are inherited, so A.T can refer to a static member defined in a parent of A. In Scala we model static members as members of the class's companion object. In addition to the inheritance class hierarchy, we must be careful to follow the implied parallel hierarchy of the companion objects.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will do

Fixed a while ago, according to comments on the issue.
Scala puts static members of Java classes into the companion
object symbol. When parsing Java code, references to static
members need to be looked up in these companions.

In 2.11.x, a synthetic `import A._` was added to Java classes
having static members, which lead to some bugs (SI-9111). The
scheme was changed in 4e822d7 (2.12.x only): when a lookup
of `T` / `A.T` fails, the typer searches in the companion of the
current class.

This fixes some issues, but not all
  - In Java, `A.T` can refer to a static member defined in a
    parent of `A`
  - If a static `A.T` exists, but a parent of `A` defines a
    non-static member class `T`, the non-static one would be
    chosen (wrongly)

This commit pushes the logic to consider companion objects into
`FindMembers` and makes sure that for ever class in the base type
sequence, the companion is considered before moving to the next
base type.
SI-4788 implemented support for `@Retention` annotations of Java
annotation classes: if a Java annotation class is itself annotated
`@Retention(value=SOURCE)`, it is not emitted to the annotated (Scala)
classfile.

However, the Java parser currently skips annotations altogether, so
if a Java annotation is passed to the Scala compiler in mixed compilation,
its `@Retention` not respected. Instead, uses of the annotation class
will be written to the classfile.

The existing tests for t4788 basically both tested separate compilation,
because partest's "mixed" compilation used to compile Scala sources twice
(the second time with the Java classfiles on the classpath). This was
fixed recently, uncovering the difference.

Additionally, the actual `t4788-separate-compilation` did not work as
intended because the filename groups were wrong (the `.scala` files should
all have been in the `_2` group.).
@@ -984,6 +985,10 @@ trait Contexts { self: Analyzer =>

def isNameInScope(name: Name) = lookupSymbol(name, _ => true).isSuccess

def isPlainClassInJavaUnit(sym: Symbol): Boolean =
unit.isJava && sym.isJavaDefined && sym.isClass && !sym.isModuleClass && !sym.isPackageClass
Copy link
Contributor

@adriaanm adriaanm Dec 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exclude sym.isModuleClass? I'm trying to get this scenario to work (using scaladoc to more eagerly force infos):

public class A {
    public static class T {}
}
public class B extends A {
    public static class U extends T { }
//                                ^
// error: not found: type T
}

// members: `A.T` can resolve to a static member defined in a parent of `A`.
// class C { class T }; class B extends C { static class T }; class A extends B
// Here, `A.T` resolves to the static class `B.T`, so we need to check B's companion before proceeding to C.
if (searchCompanions) bcs.flatMap(bc => List(bc, bc.companionModule.moduleClass))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my example above, I think we also need to support the case where we're looking for the base classes of a module class. The companion modules of its companion class's base classes should also be included.

@adriaanm adriaanm self-assigned this Dec 21, 2016
@retronym
Copy link
Member

Improvements in this area make it easier for us to typecheck more Java code, which we need to do (robustly) to improve support for annotations in jointly compiled sources (e.g. for the SI-4788 limitation noted above), or to typecheck the RHS of constant expressions.

I like your approach of addressing this at its core, in findMembers.

However, we'll have to be quite careful in making membership dependent on the context of the unit containing the reference. Will this break any caches? I can't find any problems right now. e.g. ModuleClassSymbol#implicitMembersCacheValue looks okay.

@lrytz
Copy link
Member Author

lrytz commented Jan 6, 2017

I'm going to put this on hold, busy with other issues for now. Hope to pick it up for 2.12.2.

@ghik
Copy link
Contributor

ghik commented Jul 11, 2017

Hello,

Is there anything I can do to help so that this gets fixed in, say, 2.12.4? (I assume it's already too late for 2.12.3). It really makes me frustrated, because it's a regression, blocking us from migration to Scala 2.12, significant work had already been put into it and now it seems like it's stuck in a limbo.

@lrytz
Copy link
Member Author

lrytz commented Jul 11, 2017

I'm sorry this slipped, I really need to pick it up again for 2.12.4. I have it on my list, but it just keeps getting bumped. I will take a look, keeping in mind that perfect is the enemy of good.

@guizmaii
Copy link

guizmaii commented Jul 19, 2017

Hi,

Where can we track the evolution of this bug ?

Thanks

@lrytz
Copy link
Member Author

lrytz commented Jul 19, 2017

At scala/bug#9111

@guizmaii
Copy link

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants