Skip to content

Commit

Permalink
Implicit constructors and custom init method
Browse files Browse the repository at this point in the history
  • Loading branch information
bdw429s committed Jan 6, 2024
1 parent bf560ee commit 5d57bc7
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@
import ortus.boxlang.runtime.context.ClassBoxContext;
import ortus.boxlang.runtime.context.IBoxContext;
import ortus.boxlang.runtime.dynamic.IReferenceable;
import ortus.boxlang.runtime.dynamic.casters.StringCaster;
import ortus.boxlang.runtime.runnables.IClassRunnable;
import ortus.boxlang.runtime.scopes.IntKey;
import ortus.boxlang.runtime.scopes.Key;
import ortus.boxlang.runtime.types.Array;
import ortus.boxlang.runtime.types.IType;
import ortus.boxlang.runtime.types.Struct;
import ortus.boxlang.runtime.types.exceptions.ApplicationException;
import ortus.boxlang.runtime.types.exceptions.BoxLangException;
import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException;
Expand Down Expand Up @@ -186,19 +188,24 @@ public static void setHandlesCacheEnabled( Boolean enabled ) {
*/

/**
* Invokes the constructor for the class with the given arguments and returns the instance of the object
* Invokes the constructor for the class with the given positional arguments and returns the instance of the object
*
* @param targetClass The Class that you want to invoke a constructor on
* @param args The arguments to pass to the constructor
* @param args The positional arguments to pass to the constructor
*
* @return The instance of the class
*/
public static <T> T invokeConstructor( IBoxContext context, Class<T> targetClass, Object... args ) {

Object[] BLArgs = null;
// Thou shalt not pass!
if ( isInterface( targetClass ) ) {
throw new BoxRuntimeException( "Cannot invoke a constructor on an interface" );
}
// check if targetClass is an IClassRunnable
if ( IClassRunnable.class.isAssignableFrom( targetClass ) ) {
BLArgs = args;
args = EMPTY_ARGS;
}

// Unwrap any ClassInvoker instances
unWrapArguments( args );
Expand All @@ -221,26 +228,56 @@ public static <T> T invokeConstructor( IBoxContext context, Class<T> targetClass

// If this is a Box Class, some additional initialization is needed
if ( thisInstance instanceof IClassRunnable cfc ) {
return bootstrapBLClass( context, cfc, BLArgs, null );
}
return thisInstance;
} catch ( RuntimeException e ) {
throw e;
} catch ( Throwable e ) {
throw new BoxRuntimeException( "Error invoking constructor for class " + targetClass.getName(), e );
}
}

// This class context is really only used while boostrapping the pseudoConstructor. It will NOT be used as a parent
// context once the CFC is initialized. Methods called on this CFC will have access to the variables/this scope via their
// FunctionBoxContext, but their parent context will be whatever context they are called from.
IBoxContext classContext = new ClassBoxContext( context, cfc );

// Bootstrap the pseudoConstructor
cfc.pseudoConstructor( classContext );

// Call constructor
// TODO: look for initMethod annotation
if ( cfc.dereference( context, Key.init, true ) != null ) {
Object result = cfc.dereferenceAndInvoke( classContext, Key.init, new Object[] {}, false );
// CF returns the actual result of the constructor, but I'm not sure it makes sense or if people actually ever
// return anything other than "this".
if ( result != null ) {
// This cast will fail if the init returns something like a string
return ( T ) result;
}
}
/**
* Invokes the constructor for the class with the given named arguments and returns the instance of the object
*
* @param targetClass The Class that you want to invoke a constructor on
* @param args The named arguments to pass to the constructor
*
* @return The instance of the class
*/
public static <T> T invokeConstructor( IBoxContext context, Class<T> targetClass, Map<Key, Object> args ) {

// Thou shalt not pass!
if ( isInterface( targetClass ) ) {
throw new BoxRuntimeException( "Cannot invoke a constructor on an interface" );
}
// check if targetClass is an IClassRunnable
if ( !IClassRunnable.class.isAssignableFrom( targetClass ) ) {
throw new BoxRuntimeException( "Cannot use named arguments on a Java constructor." );
}
// Unwrap any ClassInvoker instances
unWrapArguments( args );
// Method signature for a constructor is void (Object...)
MethodType constructorType = MethodType.methodType( void.class, argumentsToClasses( args ) );
// Define the bootstrap method
MethodHandle constructorHandle;
try {
constructorHandle = METHOD_LOOKUP.findConstructor( targetClass, constructorType );
} catch ( NoSuchMethodException | IllegalAccessException e ) {
throw new BoxRuntimeException( "Error getting constructor for class " + targetClass.getName(), e );
}
// Create a callsite using the constructor handle
CallSite callSite = new ConstantCallSite( constructorHandle );
// Bind the CallSite and invoke the constructor with the provided arguments
// Invoke Dynamic tries to do argument coercion, so we need to convert the arguments to the right types
MethodHandle constructorInvoker = callSite.dynamicInvoker();
try {
T thisInstance = ( T ) constructorInvoker.invokeWithArguments( EMPTY_ARGS );

// If this is a Box Class, some additional initialization is needed
if ( thisInstance instanceof IClassRunnable cfc ) {
return bootstrapBLClass( context, cfc, null, args );
}
return thisInstance;
} catch ( RuntimeException e ) {
Expand All @@ -250,6 +287,65 @@ public static <T> T invokeConstructor( IBoxContext context, Class<T> targetClass
}
}

/**
* Reusable method for bootstrapping IClassRunnables
*
* @param cfc The class to bootstrap
* @param args The arguments to pass to the constructor
*
* @return The instance of the class
*/
private static <T> T bootstrapBLClass( IBoxContext context, IClassRunnable cfc, Object[] positionalArgs, Map<Key, Object> namedArgs ) {

// This class context is really only used while boostrapping the pseudoConstructor. It will NOT be used as a parent
// context once the CFC is initialized. Methods called on this CFC will have access to the variables/this scope via their
// FunctionBoxContext, but their parent context will be whatever context they are called from.
IBoxContext classContext = new ClassBoxContext( context, cfc );

// Bootstrap the pseudoConstructor
cfc.pseudoConstructor( classContext );

// Call constructor
// look for initMethod annotation
Object initMethod = cfc.getAnnotations().get( Key.initMethod );
Key initKey;
if ( initMethod != null ) {
initKey = Key.of( StringCaster.cast( initMethod ) );
} else {
initKey = Key.init;
}
if ( cfc.dereference( context, initKey, true ) != null ) {
Object result;
if ( positionalArgs != null ) {
result = cfc.dereferenceAndInvoke( classContext, initKey, positionalArgs, false );
} else {
result = cfc.dereferenceAndInvoke( classContext, initKey, namedArgs, false );
}
// CF returns the actual result of the constructor, but I'm not sure it makes sense or if people actually ever
// return anything other than "this".
if ( result != null ) {
// This cast will fail if the init returns something like a string
return ( T ) result;
}
} else {
// implicit constructor

if ( positionalArgs != null && positionalArgs.length == 1 && positionalArgs[ 0 ] instanceof Struct named ) {
namedArgs = named.getWrapped();
} else if ( positionalArgs != null && positionalArgs.length > 0 ) {
throw new BoxRuntimeException( "Implicit constructor only accepts named args or a single Struct as a positional arg." );
}

if ( namedArgs != null ) {
// loop over args and invoke setter methods for each
for ( Map.Entry<Key, Object> entry : namedArgs.entrySet() ) {
cfc.dereferenceAndInvoke( classContext, Key.of( "set" + entry.getKey().getName() ), new Object[] { entry.getValue() }, false );
}
}
}
return ( T ) cfc;
}

/**
* Invokes the no-arg constructor for the class with the given arguments and returns the instance of the object
*
Expand Down Expand Up @@ -951,6 +1047,19 @@ private static void unWrapArguments( Object[] arguments ) {
}
}

/**
* Unwrap any ClassInvoker instances in the arguments
*
* @param arguments The arguments to unwrap
*
* @return The unwrapped arguments
*/
private static void unWrapArguments( Map<Key, Object> arguments ) {
for ( Key key : arguments.keySet() ) {
arguments.put( key, unWrap( arguments.get( key ) ) );
}
}

/**
* --------------------------------------------------------------------------
* Implementation of IReferencable
Expand Down
1 change: 1 addition & 0 deletions src/main/java/ortus/boxlang/runtime/scopes/Key.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class Key {
public static final Key _10 = Key.of( 10 );

public static final Key init = Key.of( "init" );
public static final Key initMethod = Key.of( "initMethod" );
public static final Key recordCount = Key.of( "recordCount" );
public static final Key columnList = Key.of( "columnList" );
public static final Key currentRow = Key.of( "currentRow" );
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/ortus/boxlang/runtime/types/Struct.java
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ public static Object unWrapNull( Object value ) {
return value;
}

public Map<?, ?> getWrapped() {
public Map<Key, Object> getWrapped() {
return wrapped;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
Expand Down Expand Up @@ -49,6 +51,7 @@
import ortus.boxlang.ast.statement.BoxImport;
import ortus.boxlang.ast.statement.BoxProperty;
import ortus.boxlang.runtime.config.util.PlaceholderHelper;
import ortus.boxlang.runtime.dynamic.casters.BooleanCaster;
import ortus.boxlang.runtime.scopes.Key;
import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException;
import ortus.boxlang.transpiler.JavaTranspiler;
Expand Down Expand Up @@ -591,7 +594,13 @@ public Node transform( BoxNode node, TransformerContext context ) throws Illegal
}

transpiler.popContextName();
System.out.println( entryPoint );
String text = entryPoint.toString();
String numberedText = IntStream.range( 0, text.split( "\n" ).length )
.mapToObj( index -> ( index + 1 ) + " " + text.split( "\n" )[ index ] )
.collect( Collectors.joining( "\n" ) );

// System.out.println( numberedText );

return entryPoint;
}

Expand Down Expand Up @@ -717,11 +726,20 @@ private List<Expression> transformProperties( List<BoxProperty> properties ) {
members.add( jNameKey );
members.add( javaExpr );

getterLookup.add( jGetNameKey );
getterLookup.add( ( Expression ) parseExpression( "properties.get( ${name} )", values ) );

setterLookup.add( jSetNameKey );
setterLookup.add( ( Expression ) parseExpression( "properties.get( ${name} )", values ) );
// Check if getter key annotation is defined in finalAnnotations and false
boolean getter = !finalAnnotations.stream()
.anyMatch( it -> it.getKey().getValue().equalsIgnoreCase( "getter" ) && !BooleanCaster.cast( it.getValue() ) );
if ( getter ) {
getterLookup.add( jGetNameKey );
getterLookup.add( ( Expression ) parseExpression( "properties.get( ${name} )", values ) );
}
// Check if setter key annotation is defined in finalAnnotations and false
boolean setter = !finalAnnotations.stream()
.anyMatch( it -> it.getKey().getValue().equalsIgnoreCase( "setter" ) && !BooleanCaster.cast( it.getValue() ) );
if ( setter ) {
setterLookup.add( jSetNameKey );
setterLookup.add( ( Expression ) parseExpression( "properties.get( ${name} )", values ) );
}
} );
if ( members.isEmpty() ) {
Expression emptyMap = ( Expression ) parseExpression( "Collections.emptyMap()", new HashMap<>() );
Expand Down
53 changes: 53 additions & 0 deletions src/test/java/TestCases/phase3/ClassTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -303,4 +304,56 @@ public void testProperties() {

}

@DisplayName( "Implicit Constructor named" )
@Test
@Disabled( "waiting on named arg support" )
public void testImplicitConstructorNamed() {

instance.executeStatement(
"""
cfc = new src.test.java.TestCases.phase3.ImplicitConstructorTest( name="brad", age=43, favoriteColor="blue" );
name = cfc.getName();
age = cfc.getAge();
favoriteColor = cfc.getFavoriteColor();
""", context );

assertThat( variables.get( Key.of( "name" ) ) ).isEqualTo( "brad" );
assertThat( variables.get( Key.of( "age" ) ) ).isEqualTo( 43 );
assertThat( variables.get( Key.of( "favoriteColor" ) ) ).isEqualTo( "blue" );

}

@DisplayName( "Implicit Constructor positional" )
@Test
public void testImplicitConstructorPositional() {

instance.executeStatement(
"""
cfc = new src.test.java.TestCases.phase3.ImplicitConstructorTest( {name="brad", age=43, favoriteColor="blue" });
name = cfc.getName();
age = cfc.getAge();
favoriteColor = cfc.getFavoriteColor();
""", context );

assertThat( variables.get( Key.of( "name" ) ) ).isEqualTo( "brad" );
assertThat( variables.get( Key.of( "age" ) ) ).isEqualTo( 43 );
assertThat( variables.get( Key.of( "favoriteColor" ) ) ).isEqualTo( "blue" );

}

@DisplayName( "InitMethod Test" )
@Test
public void testInitMethod() {

instance.executeStatement(
"""
cfc = new src.test.java.TestCases.phase3.InitMethodTest( );
result = cfc.getInittedProperly();
""", context );

assertThat( variables.get( Key.of( "result" ) ) ).isEqualTo( true );

}

}
6 changes: 6 additions & 0 deletions src/test/java/TestCases/phase3/ImplicitConstructorTest.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component accessors=true {
property name="name";
property name="age";
property name="favoriteColor";

}
9 changes: 9 additions & 0 deletions src/test/java/TestCases/phase3/InitMethodTest.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
component initMethod=birth accessors=true {
property inittedProperly default=false;
function init() {
throw "you'd better not call me!";
}
function birth() {
inittedProperly=true
}
}

0 comments on commit 5d57bc7

Please sign in to comment.