You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Eric Milles (Jira)" <ji...@apache.org> on 2023/07/11 18:40:00 UTC

[jira] [Comment Edited] (GROOVY-11118) Partial JEP 445 compatibility

    [ https://issues.apache.org/jira/browse/GROOVY-11118?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17742132#comment-17742132 ] 

Eric Milles edited comment on GROOVY-11118 at 7/11/23 6:39 PM:
---------------------------------------------------------------

Seems reasonable.  I'm not sure if this is relevant or not.  I once created an AST transform that would create a psvm that delegates to an instance method, like this:
{code:groovy}
class Foo {
  @Main bar() {
    println 'baz'
  }
}
{code}

Does this seem useful in light of the JEP 445 support?  If you or anyone wants to have a look, here it is:
{code:groovy}
package xforms

import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod

import java.lang.annotation.*

import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformationClass

@GroovyASTTransformationClass('xforms.MainMethodTransform')
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
@interface Main {
}

//------------------------------------------------------------------------------

@AutoFinal @GroovyASTTransformation
class MainMethodTransform implements ASTTransformation {

  @Override
  void visit(ASTNode[] nodes, SourceUnit source) {
    if (!nodes || !nodes[0] || !nodes[1]) return
    if (!(nodes[1] instanceof MethodNode)) return
    if (!(nodes[0] instanceof AnnotationNode)) return
    if (nodes[0].classNode?.name != Main.name) return
    try {
      MethodNode annotatedMethod = nodes[1]
      addGeneratedMethod(annotatedMethod.declaringClass, newMainMethod(annotatedMethod))
    } catch (e) {
      source.addException(e)
    }
  }

  private static MethodNode newMainMethod(MethodNode target) {
    def packageName = target.declaringClass.packageName
    def   className = target.declaringClass.name
    def  methodName = target.name

    def ast = new AstBuilder().buildFromString(CompilePhase.CANONICALIZATION, false, """
      ${packageName ? 'package ' + packageName : ''}

      class ${target.declaringClass.nameWithoutPackage} {
        public static void main(String[] args) {
          new ${className}().${methodName}()
        }
      }
    """)
    // ast[0] is BlockStatement
    return (ast[1] as ClassNode).methods.find { it.name == 'main' }
  }
}
{code}

And unit tests:
{code:groovy}
package xforms

import static org.codehaus.groovy.control.CompilePhase.CANONICALIZATION

import org.codehaus.groovy.tools.ast.TransformTestHelper

final class MainXformTests {
  @Test
  void test() {
    def parser = new TransformTestHelper(new MainMethodTransform(), CANONICALIZATION)
    def clazz = parser.parse """import ${Main.name}
      class ShouldHaveMain {
        @Main foo() {
          println 'bar'
        }
      }
    """
    clazz.newInstance().main()
  }

  @Test
  void 'direct test'() {
    ShouldHaveMain.main()
  }
}

class ShouldHaveMain {
  @Main foo() {
    println 'bar'
  }
}
{code}


was (Author: emilles):
It seems reasonable.  I'm not sure if this is relevant or not.  I once created an AST transform that would create a psvm that delegates to an instance method, like this:
{code:groovy}
class Foo {
  @Main bar() {
    println 'baz'
  }
}
{code}

Does this seem useful in light of the JEP 445 support?  If you or anyone wants to have a look, here it is:
{code:groovy}
package xforms

import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod

import java.lang.annotation.*

import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformationClass

@GroovyASTTransformationClass('xforms.MainMethodTransform')
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
@interface Main {
}

//------------------------------------------------------------------------------

@AutoFinal @GroovyASTTransformation
class MainMethodTransform implements ASTTransformation {

  @Override
  void visit(ASTNode[] nodes, SourceUnit source) {
    if (!nodes || !nodes[0] || !nodes[1]) return
    if (!(nodes[1] instanceof MethodNode)) return
    if (!(nodes[0] instanceof AnnotationNode)) return
    if (nodes[0].classNode?.name != Main.name) return
    try {
      MethodNode annotatedMethod = nodes[1]
      addGeneratedMethod(annotatedMethod.declaringClass, newMainMethod(annotatedMethod))
    } catch (e) {
      source.addException(e)
    }
  }

  private static MethodNode newMainMethod(MethodNode target) {
    def packageName = target.declaringClass.packageName
    def   className = target.declaringClass.name
    def  methodName = target.name

    def ast = new AstBuilder().buildFromString(CompilePhase.CANONICALIZATION, false, """
      ${packageName ? 'package ' + packageName : ''}

      class ${target.declaringClass.nameWithoutPackage} {
        public static void main(String[] args) {
          new ${className}().${methodName}()
        }
      }
    """)
    // ast[0] is BlockStatement
    return (ast[1] as ClassNode).methods.find { it.name == 'main' }
  }
}
{code}

And unit tests:
{code:groovy}
package xforms

import static org.codehaus.groovy.control.CompilePhase.CANONICALIZATION

import org.codehaus.groovy.tools.ast.TransformTestHelper

final class MainXformTests {
  @Test
  void test() {
    def parser = new TransformTestHelper(new MainMethodTransform(), CANONICALIZATION)
    def clazz = parser.parse """import ${Main.name}
      class ShouldHaveMain {
        @Main foo() {
          println 'bar'
        }
      }
    """
    clazz.newInstance().main()
  }

  @Test
  void 'direct test'() {
    ShouldHaveMain.main()
  }
}

class ShouldHaveMain {
  @Main foo() {
    println 'bar'
  }
}
{code}

> Partial JEP 445 compatibility
> -----------------------------
>
>                 Key: GROOVY-11118
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11118
>             Project: Groovy
>          Issue Type: New Feature
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>              Labels: GEP, breaking
>             Fix For: 5.x
>
>
> [JEP 445: Unnamed Classes and Instance Main Methods|https://openjdk.org/jeps/445] provides features that improve using Java for scripting purposes. While these features offer a subset of the features offered by Groovy for scripting, it would be nice to offer some interworking for cut-n-paste compatibility and leveraging the updated launch protocol.
> (i) This description will contain the most update-to-date proposal for the design but may undergo change during discussions. If needed, a separate GEP section in the wiki will capture the final design.
> h2. Scripting semantics
> ||Source file characteristics||Semantics determined||
> |Contains class declaration|Compiled as a class|
> |Contains uncontained non-declaration statements
> or a static main method|Compiled as a Groovy script|
> |Contains an instance main method, and
> possibly field and other method declarations|Compiled as a JEP 445 compatible class|
> Groovy scripts are wrapped into an encompassing class that extends {{{}groovy.lang.Script{}}}.
> Such classes have a {{public static void main}} added which (summarising) creates a class instance and calls its {{run}} method.
> Uncontained statements within the script can be thought of as belonging within the {{run}} method.
> Declaration statements within the script are treated as local variable definitions (also within the {{run}} method).
> Field definitions can be obtained by annotating declaration statements with the {{@Field}} annotation.
> JEP 445 scripts do not extend {{Script}} and do not have any methods added.
> Declaration statements within the script are treated as field definitions.
> h2. Script runner
> Groovy's runner mechanism will be extended to follow a similar protocol to Java's revised launch protocol. This should be available for JDK11+ and a JEP-445 compatible class could be invoked using JDK21+ with preview enabled (with Groovy on the classpath unless a POJO class is created).
> h2. Breaking changes
> Previously, if a script had a single instance {{main}} method not taking a {{String[]}} parameter and no loose statements, that was previously accepted as a normal Groovy script (and nothing would execute). Now if the method has an Object parameter, that is treated as a JEP-445 script and its contents executed.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)