Skip to content
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

Field access on Groovy 4 erroneously converted to getter call #1544

Open
kriegaex opened this issue Nov 20, 2022 · 8 comments
Open

Field access on Groovy 4 erroneously converted to getter call #1544

kriegaex opened this issue Nov 20, 2022 · 8 comments

Comments

@kriegaex
Copy link
Contributor

kriegaex commented Nov 20, 2022

Describe the bug

I upgraded an old playground project from Groovy 3 to 4. Now, a field access like alphabet[random.nextInt(alphabet.length())] is converted to something like a bogus getAlphabet() call, yielding:

groovy.lang.MissingMethodException: No signature of method: de.scrum_master.stackoverflow.q71631373.FirstEvent.getAlphabet() is applicable for argument types: () values: []

	at de.scrum_master.stackoverflow.q71631373.BaseEvent.init_closure1(DynamicBaseClassTest.groovy:58)
	at groovy.lang.Closure.call(Closure.java:418)
	at groovy.lang.Closure.call(Closure.java:434)
	at de.scrum_master.stackoverflow.q71631373.BaseEvent.init(DynamicBaseClassTest.groovy:58)
	at org.spockframework.mock.runtime.ByteBuddyMethodInvoker.respond(ByteBuddyMethodInvoker.java:20)
	at org.spockframework.mock.runtime.MockInvocation.callRealMethod(MockInvocation.java:61)
	at org.spockframework.mock.CallRealMethodResponse.respond(CallRealMethodResponse.java:30)
	at org.spockframework.mock.runtime.MockController.handle(MockController.java:50)
	at org.spockframework.mock.runtime.JavaMockInterceptor.intercept(JavaMockInterceptor.java:86)
	at org.spockframework.mock.runtime.ByteBuddyInterceptorAdapter.interceptNonAbstract(ByteBuddyInterceptorAdapter.java:35)
	at de.scrum_master.stackoverflow.q71631373.DynamicBaseClassTest.basic event class functionality(DynamicBaseClassTest.groovy:14)

To Reproduce

Try it in the Groovy Web Console. Sorry for not minimising the spec, I simply copied an old spec from a Stack Overflow answer I wrote a while ago.

package de.scrum_master.stackoverflow.q71631373

import spock.lang.Specification
import spock.lang.Unroll

class DynamicBaseClassTest extends Specification {
  @Unroll("verify #className")
  def "basic event class functionality"() {
    given:
    def service = new Service()
    def event = Spy(baseEventClass.getConstructor().newInstance())

    when:
    event.init()
    then:
    // '.id' and '.name' should be enough, but on Spock 2.1 there is a problem
    // when not explicitly using the '@' notation for direct field access.
    event.@id > 0
    event.@name.length() == 10

    when:
    service.sendEvent(event)
    then:
    1 * event.sendExternalEvent(_, event.class.name, [:])

    where:
    baseEventClass << getEventClasses()
    className = baseEventClass.simpleName
  }

  static List<Class<? extends BaseEvent>> getEventClasses() {
    [FirstEvent, SecondEvent, ThirdEvent]
  }
}

interface Event {
  void init()

  void sendExternalEvent(String id, String className, Map options)
}

class Service {
  void sendEvent(Event event) {
    event.sendExternalEvent("123", event.class.name, [:])
  }
}

abstract class BaseEvent implements Event {
  private static final Random random = new Random()
  private static final String alphabet = (('A'..'Z') + ('0'..'9')).join()

  protected int id
  protected String name

  @Override
  void init() {
    id = 1 + random.nextInt(100)
    name = (1..10).collect { alphabet[random.nextInt(alphabet.length())] }.join()
  }
}

class FirstEvent extends BaseEvent {
  @Override
  void sendExternalEvent(String id, String className, Map options) {}

  String doFirst() { "first" }
}

class SecondEvent extends BaseEvent {
  @Override
  void sendExternalEvent(String id, String className, Map options) {}

  String doSecond() { "second" }
}

class ThirdEvent extends BaseEvent {
  @Override
  void sendExternalEvent(String id, String className, Map options) {}

  int doThird() { 3 }
}

Expected behavior

The spec passes on Groovy 4.

Actual behavior

The spec fails on Groovy 4, while on 2.5 and 3 it passes. Therefore, I have deactivated the spec for now like this:

// TODO: Remove @Requires after https://github.com/spockframework/spock/issues/1544 is fixed
@Requires({ GroovySystem.shortVersion as double < 4.0 })

Java version

17

Buildtool version

Maven 3.8.1

What operating system are you using

Windows

Dependencies

Spock 2.3, Groovy 4. See GWC configuration.

Additional context

No response

@kriegaex kriegaex added the bug label Nov 20, 2022
@kriegaex kriegaex changed the title Field access on Groovy 4 erroneously converted to setter call Field access on Groovy 4 erroneously converted to getter call Nov 20, 2022
@AndreasTu
Copy link
Member

@kriegaex I think this is not a Spock issue it is more a Groovy 4 issue.

If I strip down to sample to only use Groovy and not Spock, it also fails with the same exception.
groovy.lang.MissingPropertyException: No such property: alphabet for class: de.scrum_master.stackoverflow.q71631373.FirstEvent

See Groovy Web Console

Sample code:

package de.scrum_master.stackoverflow.q71631373

abstract class BaseEvent{
  private static final Random random = new Random()
  private static final String alphabet = (('A'..'Z') + ('0'..'9')).join()

  protected int id
  protected String name

  void init() {
    id = 1 + random.nextInt(100)
    name = (1..10).collect { alphabet[random.nextInt(alphabet.length())] }.join()
  }
}

class FirstEvent extends BaseEvent {
}

def e = new FirstEvent()
e.init() //<-- Fails with exception in Groovy 4 but not in Groovy 3

@kriegaex
Copy link
Contributor Author

kriegaex commented Aug 1, 2023

@AndreasTu, that is an interesting find. You seem to be an experienced Groovy developer. Would you mind creating a Groovy issue? I have not tried in any version higher than 4.0.9 (GWC version, latest Groovy release is 4.0.13), but assuming that the problem still occurs in the latest release, an issue should be created, linking to this one, so we can track it.

@AndreasTu
Copy link
Member

I have created GROOVY-11144 for that.

@AndreasTu
Copy link
Member

The issue is fixed in Groovy 5.0.0-alpha-1 and it seams that there will not be a fix in Groovy 4.
As commented in the Groovy ticket, you can use @CompileStatic as workaround.

I guess we can close this ticket here.

@kriegaex
Copy link
Contributor Author

kriegaex commented Aug 2, 2023

I still have hope that they might fix it for Groovy 4, because the latter is the current release and the current behaviour clearly a bug.

For the record, a few workarounds:

Workaround 1: Use class name when referring to static fields from within a closure.

abstract class BaseEvent implements Event {
  private static final Random random = new Random()
  private static final String alphabet = (('A'..'Z') + ('0'..'9')).join()

  protected int id
  protected String name

  @Override
  void init() {
    id = 1 + random.nextInt(100)
    name = (1..10).collect { BaseEvent.alphabet[BaseEvent.random.nextInt(BaseEvent.alphabet.length())] }.join()
  }
}

Workaround 2: Use @CompileStatic. Note: We also need to use join('').

@CompileStatic
abstract class BaseEvent implements Event {
  private static final Random random = new Random()
  private static final String alphabet = (('A'..'Z') + ('0'..'9')).join('')

  protected int id
  protected String name

  @Override
  void init() {
    id = 1 + random.nextInt(100)
    name = (1..10).collect { alphabet[random.nextInt(alphabet.length())] }.join('')
  }
}

Workaround 3: Create non-private getters for static fields.

abstract class BaseEvent implements Event {
  private static final Random random = new Random()
  private static final String alphabet = (('A'..'Z') + ('0'..'9')).join()

  protected static Random getRandom() { random }
  protected static String getAlphabet() { alphabet }

  protected int id
  protected String name

  @Override
  void init() {
    id = 1 + random.nextInt(100)
    name = (1..10).collect { alphabet[random.nextInt(alphabet.length())] }.join()
  }
}

My favourite is no. 3, because for no. 1 the IDE warns about redundant type name references, which easily get optimised away by automatic refactorings, and in no. 2 the @CompileStatic might not always be a good choice, depending on the class definition.

@kriegaex
Copy link
Contributor Author

kriegaex commented Aug 2, 2023

@leonard84, DYT it would make sense to leave the issue open until the problem was fixed in Groovy 4 and then there is a Spock release using the fixed version?

@blackdrag
Copy link

blackdrag commented Aug 2, 2023

@kriegaex the DefaultGroovyMethods should not be required in case 2. You have to use join("") though since there is no join() method

@kriegaex
Copy link
Contributor Author

kriegaex commented Aug 6, 2023

@blackdrag, thank you. You are absolutely right. I played around with the variants and did not prune it back to the minimal change necessary. I updated my sample code and the corresponding comments.

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

No branches or pull requests

3 participants