diff --git a/build.gradle b/build.gradle index bc36c8f58..a0f306fbb 100644 --- a/build.gradle +++ b/build.gradle @@ -128,7 +128,7 @@ dependencies { // https://mvnrepository.com/artifact/org.slf4j/slf4j-api implementation 'org.slf4j:slf4j-api:2.0.16' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - implementation 'ch.qos.logback:logback-classic:1.5.8' + implementation 'ch.qos.logback:logback-classic:1.5.10' // https://mvnrepository.com/artifact/com.zaxxer/HikariCP implementation 'com.zaxxer:HikariCP:6.0.0' // https://mvnrepository.com/artifact/org.ow2.asm/asm-tree diff --git a/gradle.properties b/gradle.properties index c94a19d18..3689f5ebf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -#Fri Oct 04 18:10:20 UTC 2024 +#Fri Oct 11 15:26:50 UTC 2024 antlrVersion=4.13.1 jdkVersion=21 -version=1.0.0-beta18 +version=1.0.0-beta19 diff --git a/src/main/java/ortus/boxlang/compiler/BXCompiler.java b/src/main/java/ortus/boxlang/compiler/BXCompiler.java index 346ead9bb..658080c3c 100644 --- a/src/main/java/ortus/boxlang/compiler/BXCompiler.java +++ b/src/main/java/ortus/boxlang/compiler/BXCompiler.java @@ -166,7 +166,7 @@ public static void main( String[] args ) { } } - private static void compileFile( Path sourcePath, Path targetPath, Boolean stopOnError, BoxRuntime runtime, Path basePath, String mapping ) { + public static void compileFile( Path sourcePath, Path targetPath, Boolean stopOnError, BoxRuntime runtime, Path basePath, String mapping ) { try { Path directoryPath = targetPath.getParent(); if ( directoryPath != null && !Files.exists( directoryPath ) ) { diff --git a/src/main/java/ortus/boxlang/compiler/IBoxpiler.java b/src/main/java/ortus/boxlang/compiler/IBoxpiler.java index d5d3afde3..4cefd410c 100644 --- a/src/main/java/ortus/boxlang/compiler/IBoxpiler.java +++ b/src/main/java/ortus/boxlang/compiler/IBoxpiler.java @@ -21,11 +21,13 @@ public interface IBoxpiler { - static final Set RESERVED_WORDS = new HashSet<>( Arrays.asList( "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", + static final Set RESERVED_WORDS = new HashSet<>( Arrays.asList( "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while" ) ); + static final Pattern FQNBasePattern = Pattern.compile( "(.*?)(\\$Closure_.*|\\$Func_.*|\\$Lambda_.*)$" ); + /** * Generate an MD5 hash. * TODO: Move to util class @@ -80,7 +82,7 @@ static String MD5( String md5 ) { static String getBaseFQN( String FQN ) { // If fqn ends with $Cloure_xxx or $Func_xxx, $Lambda_xxx, then we need to strip that off to get the original FQN - Matcher m = Pattern.compile( "(.*?)(\\$Closure_.*|\\$Func_.*|\\$Lambda_.*)$" ).matcher( FQN ); + Matcher m = FQNBasePattern.matcher( FQN ); if ( m.find() ) { FQN = m.group( 1 ); } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/ASMBoxpiler.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/ASMBoxpiler.java index 20085a6e1..b4b13e98e 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/ASMBoxpiler.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/ASMBoxpiler.java @@ -14,6 +14,7 @@ import ortus.boxlang.compiler.Boxpiler; import ortus.boxlang.compiler.ClassInfo; import ortus.boxlang.compiler.ast.BoxClass; +import ortus.boxlang.compiler.ast.BoxInterface; import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.BoxScript; import ortus.boxlang.compiler.ast.visitor.QueryEscapeSingleQuoteVisitor; @@ -130,8 +131,10 @@ private void doCompileClassInfo( Transpiler transpiler, ClassInfo classInfo, Box classNode = transpiler.transpile( boxScript ); } else if ( node instanceof BoxClass boxClass ) { classNode = transpiler.transpile( boxClass ); + } else if ( node instanceof BoxInterface boxInterface ) { + classNode = transpiler.transpile( boxInterface ); } else { - throw new IllegalStateException( "Unexpected root type: " + node ); + throw new IllegalStateException( "Unexpected root type: " + node.getClass() + ": " + node ); } transpiler.getAuxiliary().forEach( consumer ); consumer.accept( classInfo.fqn().toString(), classNode ); diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java index 2a1ea7f8d..e534066a5 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; @@ -17,26 +19,460 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; -import org.objectweb.asm.tree.VarInsnNode; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxClass; +import ortus.boxlang.compiler.ast.BoxExpression; +import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.BoxStatement; +import ortus.boxlang.compiler.ast.expression.BoxArgument; +import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxReturnType; +import ortus.boxlang.compiler.ast.statement.BoxType; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.dynamic.Referencer; +import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.runnables.BoxClassSupport; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.AbstractFunction; +import ortus.boxlang.runtime.types.Argument; +import ortus.boxlang.runtime.types.DefaultExpression; +import ortus.boxlang.runtime.types.Function; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.Struct; +import ortus.boxlang.runtime.types.util.MapHelper; import ortus.boxlang.runtime.util.ResolvedFilePath; public class AsmHelper { + public static List generateMapOfAbstractMethodNames( Transpiler transpiler, BoxNode classOrInterface ) { + List> methodKeyLists = classOrInterface.getDescendantsOfType( BoxFunctionDeclaration.class ) + .stream() + .filter( func -> func.getBody() == null ) + .map( func -> { + List> absFunc = List.of( + transpiler.createKey( func.getName() ), + createAbstractFunction( transpiler, func ) + ); + + return absFunc; + } ) + .flatMap( x -> x.stream() ) + .collect( java.util.stream.Collectors.toList() ); + + List nodes = new ArrayList(); + + nodes.addAll( AsmHelper.array( Type.getType( Object.class ), methodKeyLists ) ); + + nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( MapHelper.class ), + "LinkedHashMapOfAny", + Type.getMethodDescriptor( Type.getType( Map.class ), Type.getType( Object[].class ) ), + false ) ); + + return nodes; + } + + private static List generateSetOfCompileTimeMethodNames( Transpiler transpiler, BoxClass boxClass ) { + List> methodKeyLists = boxClass.getDescendantsOfType( BoxFunctionDeclaration.class ) + .stream() + .map( BoxFunctionDeclaration::getName ) + .map( transpiler::createKey ) + .collect( java.util.stream.Collectors.toList() ); + + List nodes = new ArrayList(); + + nodes.addAll( AsmHelper.array( Type.getType( Key.class ), methodKeyLists ) ); + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Set.class ), + "of", + Type.getMethodDescriptor( Type.getType( Set.class ), Type.getType( Object[].class ) ), + true + ) + ); + + return nodes; + + } + + private static String getFunctionReturnType( BoxReturnType returnType ) { + if ( returnType == null || returnType.getType() == null ) { + return "any"; + } + + if ( returnType.getType().equals( BoxType.Fqn ) ) { + return returnType.getFqn(); + } + + return returnType.getType().name(); + } + + public static List createAbstractFunction( Transpiler transpiler, BoxFunctionDeclaration func ) { + List nodes = new ArrayList(); + + nodes.add( new TypeInsnNode( Opcodes.NEW, Type.getInternalName( AbstractFunction.class ) ) ); + nodes.add( new InsnNode( Opcodes.DUP ) ); + + // args + // Key name + nodes.addAll( transpiler.createKey( func.getName() ) ); + // Argument[] arguments + List> argList = func.getArgs() + .stream() + .map( arg -> transpiler.transform( arg, TransformerContext.NONE ) ) + .toList(); + nodes.addAll( AsmHelper.array( Type.getType( Argument.class ), argList ) ); + + nodes.add( new LdcInsnNode( getFunctionReturnType( func.getType() ) ) ); + + String accessModifier = "PUBLIC"; + + if ( func.getAccessModifier() != null ) { + accessModifier = func.getAccessModifier().name().toUpperCase(); + } + // Access access + nodes.add( + new FieldInsnNode( + Opcodes.GETSTATIC, + Type.getInternalName( Function.Access.class ), + accessModifier, + Type.getDescriptor( Function.Access.class ) + ) + ); + // IStruct annotations + // TODO + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, + Type.getInternalName( Struct.class ), + "EMPTY", + Type.getDescriptor( IStruct.class ) ) ); + // IStruct documentation + // TODO + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, + Type.getInternalName( Struct.class ), + "EMPTY", + Type.getDescriptor( IStruct.class ) ) ); + // String sourceObjectName + nodes.add( new LdcInsnNode( transpiler.getProperty( "boxClassName" ) ) ); + // String sourceObjectType + nodes.add( new LdcInsnNode( "class" ) ); + + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESPECIAL, + Type.getInternalName( AbstractFunction.class ), + "", + Type.getMethodDescriptor( + Type.VOID_TYPE, + Type.getType( Key.class ), + Type.getType( Argument[].class ), + Type.getType( String.class ), + Type.getType( Function.Access.class ), + Type.getType( IStruct.class ), + Type.getType( IStruct.class ), + Type.getType( String.class ), + Type.getType( String.class ) + ), + false + ) + ); + + return nodes; + } + + public static void addDebugLabel( List nodes, String label ) { + if ( !ASMBoxpiler.DEBUG ) { + return; + } + + nodes.add( new LdcInsnNode( label ) ); + nodes.add( new InsnNode( Opcodes.POP ) ); + } + + public static List getDefaultExpression( AsmTranspiler transpiler, BoxExpression body ) { + Type type = Type.getType( "L" + transpiler.getProperty( "packageName" ).replace( '.', '/' ) + + "/" + transpiler.getProperty( "classname" ) + + "$Lambda_" + transpiler.incrementAndGetLambdaCounter() + ";" ); + + ClassNode classNode = new ClassNode(); + + classNode.visit( + Opcodes.V17, + Opcodes.ACC_PUBLIC, + type.getInternalName(), + null, + Type.getInternalName( Object.class ), + new String[] { Type.getInternalName( DefaultExpression.class ) } ); + + MethodVisitor initVisitor = classNode.visitMethod( Opcodes.ACC_PUBLIC, + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + null, + null ); + initVisitor.visitCode(); + initVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + initVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( Object.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + initVisitor.visitInsn( Opcodes.RETURN ); + initVisitor.visitEnd(); + + MethodContextTracker t = new MethodContextTracker( false ); + transpiler.addMethodContextTracker( t ); + // Object evaluate( IBoxContext context ); + MethodVisitor methodVisitor = classNode.visitMethod( + Opcodes.ACC_PUBLIC, + "evaluate", + Type.getMethodDescriptor( Type.getType( Object.class ), Type.getType( IBoxContext.class ) ), + null, + null ); + methodVisitor.visitCode(); + + t.trackNewContext(); + + transpiler.transform( body, TransformerContext.NONE, ReturnValueContext.VALUE_OR_NULL ) + .forEach( ( ins ) -> ins.accept( methodVisitor ) ); + + methodVisitor.visitInsn( Opcodes.ARETURN ); + methodVisitor.visitMaxs( 0, 0 ); + methodVisitor.visitEnd(); + + transpiler.popMethodContextTracker(); + + transpiler.setAuxiliary( type.getClassName(), classNode ); + + List nodes = new ArrayList(); + + nodes.add( new TypeInsnNode( Opcodes.NEW, type.getInternalName() ) ); + nodes.add( new InsnNode( Opcodes.DUP ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKESPECIAL, + type.getInternalName(), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ) ); + return nodes; + } + + public static List callinvokeFunction( + Transpiler transpiler, + Type invokeType, + List args, + List name, + TransformerContext context, + boolean safe ) { + List nodes = new ArrayList(); + + nodes.addAll( name ); + + // handle positional args + if ( args.size() == 0 || args.get( 0 ).getName() == null ) { + nodes.addAll( + AsmHelper.array( Type.getType( Object.class ), args, + ( argument, i ) -> transpiler.transform( args.get( i ), context, ReturnValueContext.VALUE ) ) + ); + + nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "invokeFunction", + Type.getMethodDescriptor( Type.getType( Object.class ), invokeType, Type.getType( Object[].class ) ), + true ) ); + + return nodes; + } + + List> keyValues = args.stream() + .map( arg -> { + List> kv = List.of( + transpiler.createKey( arg.getName() ), + transpiler.transform( arg, context, ReturnValueContext.VALUE ) + ); + + return kv; + } ) + .flatMap( x -> x.stream() ) + .collect( Collectors.toList() ); + + nodes.addAll( AsmHelper.array( Type.getType( Object.class ), keyValues ) ); + + nodes.add( + new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( Struct.class ), + "of", + Type.getMethodDescriptor( Type.getType( IStruct.class ), Type.getType( Object[].class ) ), + false + ) + ); + + nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "invokeFunction", + Type.getMethodDescriptor( Type.getType( Object.class ), invokeType, Type.getType( Map.class ) ), + true ) ); + + return nodes; + + } + + public static List callReferencerGetAndInvoke( + Transpiler transpiler, + List args, + String name, + TransformerContext context, + boolean safe ) { + return callReferencerGetAndInvoke( transpiler, args, transpiler.createKey( name ), context, safe ); + } + + public static List callReferencerGetAndInvoke( + Transpiler transpiler, + List args, + List name, + TransformerContext context, + boolean safe ) { + List nodes = new ArrayList(); + + nodes.addAll( name ); + + // handle positional args + if ( args.size() == 0 || args.get( 0 ).getName() == null ) { + nodes.addAll( + AsmHelper.array( Type.getType( Object.class ), args, + ( argument, i ) -> transpiler.transform( args.get( i ), context, ReturnValueContext.VALUE ) ) + ); + + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, Type.getInternalName( Boolean.class ), safe ? "TRUE" : "FALSE", + Type.getDescriptor( Boolean.class ) ) ); + + nodes.add( new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Referencer.class ), + "getAndInvoke", + Type.getMethodDescriptor( Type.getType( Object.class ), + Type.getType( IBoxContext.class ), + Type.getType( Object.class ), + Type.getType( Key.class ), + Type.getType( Object[].class ), + Type.getType( Boolean.class ) + ), + false ) + ); + + return nodes; + } + + List> keyValues = args.stream() + .map( arg -> { + List> kv = List.of( + transpiler.createKey( arg.getName() ), + transpiler.transform( arg, context, ReturnValueContext.VALUE ) + ); + + return kv; + } ) + .flatMap( x -> x.stream() ) + .collect( Collectors.toList() ); + + nodes.addAll( AsmHelper.array( Type.getType( Object.class ), keyValues ) ); + + nodes.add( + new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( Struct.class ), + "of", + Type.getMethodDescriptor( Type.getType( IStruct.class ), Type.getType( Object[].class ) ), + false + ) + ); + + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, Type.getInternalName( Boolean.class ), safe ? "TRUE" : "FALSE", + Type.getDescriptor( Boolean.class ) ) ); + + nodes.add( new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Referencer.class ), + "getAndInvoke", + Type.getMethodDescriptor( Type.getType( Object.class ), + Type.getType( IBoxContext.class ), + Type.getType( Object.class ), + Type.getType( Key.class ), + Type.getType( Map.class ), + Type.getType( Boolean.class ) + ), + false ) + ); + + return nodes; + + } + + public static List callDynamicObjectInvokeConstructor( Transpiler transpiler, List args, TransformerContext context ) { + List nodes = new ArrayList(); + + // handle positional args + if ( args.size() == 0 || args.get( 0 ).getName() == null ) { + nodes.addAll( + AsmHelper.array( Type.getType( Object.class ), args, + ( argument, i ) -> transpiler.transform( args.get( i ), context, ReturnValueContext.VALUE ) ) + ); + + nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( DynamicObject.class ), + "invokeConstructor", + Type.getMethodDescriptor( Type.getType( DynamicObject.class ), + Type.getType( IBoxContext.class ), + Type.getType( Object[].class ) ), + false ) ); + + return nodes; + } + + List> keyValues = args.stream() + .map( arg -> { + List> kv = List.of( + transpiler.createKey( arg.getName() ), + transpiler.transform( arg, context, ReturnValueContext.VALUE ) + ); + + return kv; + } ) + .flatMap( x -> x.stream() ) + .collect( Collectors.toList() ); + + nodes.addAll( AsmHelper.array( Type.getType( Object.class ), keyValues ) ); + + nodes.add( + new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( Struct.class ), + "of", + Type.getMethodDescriptor( Type.getType( IStruct.class ), Type.getType( Object[].class ) ), + false + ) + ); + + nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( DynamicObject.class ), + "invokeConstructor", + Type.getMethodDescriptor( Type.getType( DynamicObject.class ), + Type.getType( IBoxContext.class ), + Type.getType( Map.class ) ), + false ) ); + + return nodes; + + } + public static void init( ClassVisitor classVisitor, boolean singleton, Type type, Type superClass, Consumer onConstruction, Type... interfaces ) { classVisitor.visit( @@ -134,7 +570,12 @@ private static void addGetInstance( ClassVisitor classVisitor, Type type ) { public static void addStaticFieldGetterWithStaticGetter( ClassVisitor classVisitor, Type type, String field, String method, String staticMethod, Type property, Object value ) { - addStaticFieldGetter( classVisitor, type, field, method, property, value ); + addStaticFieldGetterWithStaticGetter( classVisitor, type, field, method, staticMethod, property, value, true ); + } + + public static void addStaticFieldGetterWithStaticGetter( ClassVisitor classVisitor, Type type, String field, String method, String staticMethod, + Type property, Object value, boolean isFinal ) { + addStaticFieldGetter( classVisitor, type, field, method, property, value, isFinal ); MethodVisitor methodVisitor = classVisitor.visitMethod( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, staticMethod, Type.getMethodDescriptor( property ), @@ -151,7 +592,11 @@ public static void addStaticFieldGetterWithStaticGetter( ClassVisitor classVisit } public static void addStaticFieldGetter( ClassVisitor classVisitor, Type type, String field, String method, Type property, Object value ) { - FieldVisitor fieldVisitor = classVisitor.visitField( Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_PUBLIC, + addStaticFieldGetter( classVisitor, type, field, method, property, value, true ); + } + + public static void addStaticFieldGetter( ClassVisitor classVisitor, Type type, String field, String method, Type property, Object value, boolean isFinal ) { + FieldVisitor fieldVisitor = classVisitor.visitField( Opcodes.ACC_STATIC | ( isFinal ? Opcodes.ACC_FINAL : 0 ) | Opcodes.ACC_PUBLIC, field, property.getDescriptor(), null, @@ -195,6 +640,29 @@ public static void addFieldGetter( ClassVisitor classVisitor, Type type, String methodVisitor.visitEnd(); } + public static void addPrviateStaticFieldGetter( ClassVisitor classVisitor, Type type, String field, String method, Type property, Object value ) { + FieldVisitor fieldVisitor = classVisitor.visitField( Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, + field, + property.getDescriptor(), + null, + value ); + fieldVisitor.visitEnd(); + MethodVisitor methodVisitor = classVisitor.visitMethod( Opcodes.ACC_PUBLIC, + method, + Type.getMethodDescriptor( property ), + null, + null ); + methodVisitor.visitCode(); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + field, + property.getDescriptor() ); + methodVisitor.visitInsn( property.getOpcode( Opcodes.IRETURN ) ); + methodVisitor.visitMaxs( 0, 0 ); + methodVisitor.visitEnd(); + } + public static void addFieldGetterAndSetter( ClassVisitor classVisitor, Type type, String field, String getter, String setter, Type property, Object value, Consumer onAfterSet ) { addFieldGetter( classVisitor, type, field, getter, property, value ); @@ -294,7 +762,7 @@ public static void methodWithContextAndClassLocator( ClassNode classNode, null ); methodVisitor.visitCode(); // start tacking the context - new VarInsnNode( Opcodes.ALOAD, isStatic ? 0 : 1 ).accept( methodVisitor ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, isStatic ? 0 : 1 ); tracker.trackNewContext().forEach( ( node ) -> node.accept( methodVisitor ) ); methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, @@ -312,9 +780,9 @@ public static void methodWithContextAndClassLocator( ClassNode classNode, nodes.forEach( node -> node.accept( methodVisitor ) ); } - if ( implicityReturnNull ) { + if ( implicityReturnNull && !returnType.equals( Type.VOID_TYPE ) ) { // push a null onto the stack so that we can return it if there isn't an explicity return - new InsnNode( Opcodes.ACONST_NULL ).accept( methodVisitor ); + methodVisitor.visitInsn( Opcodes.ACONST_NULL ); } methodVisitor.visitInsn( returnType.getOpcode( Opcodes.IRETURN ) ); @@ -322,8 +790,8 @@ public static void methodWithContextAndClassLocator( ClassNode classNode, // TODO needs to only use try catches that match labels in the above node list // TODO should only clear the used nodes - transpiler.getTryCatchStack().stream().forEach( ( tryNode ) -> tryNode.accept( methodVisitor ) ); - transpiler.clearTryCatchStack(); + tracker.getTryCatchStack().stream().forEach( ( tryNode ) -> tryNode.accept( methodVisitor ) ); + tracker.clearTryCatchStack(); methodVisitor.visitEnd(); transpiler.popMethodContextTracker(); } @@ -339,9 +807,16 @@ public static List array( Type type, List values, BiFun for ( int i = 0; i < values.size(); i++ ) { nodes.add( new InsnNode( Opcodes.DUP ) ); nodes.add( new LdcInsnNode( i ) ); - nodes.addAll( transformer.apply( values.get( i ), i ) ); + + List toAdd = transformer.apply( values.get( i ), i ); + if ( toAdd.size() == 0 ) { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } + + nodes.addAll( toAdd ); nodes.add( new InsnNode( Opcodes.AASTORE ) ); } + return nodes; } @@ -413,7 +888,7 @@ public static MethodNode dereferenceAndInvoke( String name, Type descriptor, Typ "getInstance", Type.getMethodDescriptor( Type.getType( BoxRuntime.class ) ), false ); - node.visitMethodInsn( Opcodes.INVOKESTATIC, + node.visitMethodInsn( Opcodes.INVOKEVIRTUAL, Type.getInternalName( BoxRuntime.class ), "getRuntimeContext", Type.getMethodDescriptor( Type.getType( IBoxContext.class ) ), @@ -439,6 +914,8 @@ public static MethodNode dereferenceAndInvoke( String name, Type descriptor, Typ node.visitLdcInsn( index ); node.visitVarInsn( descriptor.getArgumentTypes()[ index ].getOpcode( Opcodes.ILOAD ), offset ); // TODO: boxing of primitives + node.visitLdcInsn( "DEBUG - ASMHelper 451" ); + node.visitInsn( Opcodes.POP ); node.visitInsn( Opcodes.AASTORE ); offset += descriptor.getArgumentTypes()[ index ].getSize(); } @@ -463,4 +940,51 @@ public static MethodNode dereferenceAndInvoke( String name, Type descriptor, Typ return node; } + + public static void addLazySingleton( ClassVisitor classVisitor, Type type, Consumer instantiation, Type... arguments ) { + classVisitor.visitField( Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, + "instance", + type.getDescriptor(), + null, + null ).visitEnd(); + MethodVisitor methodVisitor = classVisitor.visitMethod( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, + "getInstance", + Type.getMethodDescriptor( type, arguments ), + null, + null ); + + methodVisitor.visitCode(); + Label endOfMethod = new Label(); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitJumpInsn( Opcodes.IFNONNULL, endOfMethod ); + methodVisitor.visitLdcInsn( type ); + methodVisitor.visitInsn( Opcodes.MONITORENTER ); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + Label start = new Label(), end = new Label(), handler = new Label(); + methodVisitor.visitTryCatchBlock( start, end, handler, null ); + methodVisitor.visitLabel( start ); + methodVisitor.visitJumpInsn( Opcodes.IFNONNULL, end ); + instantiation.accept( methodVisitor ); + methodVisitor.visitLabel( end ); + methodVisitor.visitLdcInsn( type ); + methodVisitor.visitInsn( Opcodes.MONITOREXIT ); + methodVisitor.visitLabel( endOfMethod ); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitInsn( Opcodes.ARETURN ); + methodVisitor.visitLabel( handler ); + methodVisitor.visitLdcInsn( type ); + methodVisitor.visitInsn( Opcodes.MONITOREXIT ); + methodVisitor.visitInsn( Opcodes.ATHROW ); + methodVisitor.visitMaxs( 0, 0 ); + methodVisitor.visitEnd(); + } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmTranspiler.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmTranspiler.java index b5aaef0e6..32ccc6ef6 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmTranspiler.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmTranspiler.java @@ -1,11 +1,11 @@ package ortus.boxlang.compiler.asmboxpiler; -import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Stream; +import java.util.Objects; +import java.util.Set; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -15,7 +15,6 @@ import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; @@ -33,6 +32,7 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxComparisonOperationTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxContinueTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxDecimalLiteralTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxExpressionInvocationTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxExpressionStatementTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxFQNTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxFunctionInvocationTransformer; @@ -47,6 +47,8 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxReturnTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxScopeTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxStatementBlockTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxStaticAccessTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxStaticMethodInvocationTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxStringConcatTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxStringInterpolationTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxStringLiteralTransformer; @@ -56,13 +58,19 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.expression.BoxUnaryOperationTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxAssertTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxBufferOutputTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxClassTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxComponentTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxDoTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxForInTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxForIndexTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxFunctionDeclarationTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxIfElseTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxInterfaceTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxParamTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxRethrowTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxScriptIslandTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxStaticInitializerTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxTemplateIslandTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxThrowTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxTryTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxWhileTransformer; @@ -71,6 +79,7 @@ import ortus.boxlang.compiler.ast.BoxInterface; import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.BoxScript; +import ortus.boxlang.compiler.ast.BoxStaticInitializer; import ortus.boxlang.compiler.ast.Source; import ortus.boxlang.compiler.ast.SourceFile; import ortus.boxlang.compiler.ast.expression.BoxArgument; @@ -83,6 +92,7 @@ import ortus.boxlang.compiler.ast.expression.BoxComparisonOperation; import ortus.boxlang.compiler.ast.expression.BoxDecimalLiteral; import ortus.boxlang.compiler.ast.expression.BoxDotAccess; +import ortus.boxlang.compiler.ast.expression.BoxExpressionInvocation; import ortus.boxlang.compiler.ast.expression.BoxFQN; import ortus.boxlang.compiler.ast.expression.BoxFunctionInvocation; import ortus.boxlang.compiler.ast.expression.BoxIdentifier; @@ -93,6 +103,8 @@ import ortus.boxlang.compiler.ast.expression.BoxNull; import ortus.boxlang.compiler.ast.expression.BoxParenthesis; import ortus.boxlang.compiler.ast.expression.BoxScope; +import ortus.boxlang.compiler.ast.expression.BoxStaticAccess; +import ortus.boxlang.compiler.ast.expression.BoxStaticMethodInvocation; import ortus.boxlang.compiler.ast.expression.BoxStringConcat; import ortus.boxlang.compiler.ast.expression.BoxStringInterpolation; import ortus.boxlang.compiler.ast.expression.BoxStringLiteral; @@ -112,36 +124,34 @@ import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; import ortus.boxlang.compiler.ast.statement.BoxIfElse; import ortus.boxlang.compiler.ast.statement.BoxImport; +import ortus.boxlang.compiler.ast.statement.BoxParam; import ortus.boxlang.compiler.ast.statement.BoxProperty; import ortus.boxlang.compiler.ast.statement.BoxRethrow; import ortus.boxlang.compiler.ast.statement.BoxReturn; -import ortus.boxlang.compiler.ast.statement.BoxReturnType; +import ortus.boxlang.compiler.ast.statement.BoxScriptIsland; import ortus.boxlang.compiler.ast.statement.BoxStatementBlock; import ortus.boxlang.compiler.ast.statement.BoxSwitch; import ortus.boxlang.compiler.ast.statement.BoxThrow; import ortus.boxlang.compiler.ast.statement.BoxTry; -import ortus.boxlang.compiler.ast.statement.BoxType; import ortus.boxlang.compiler.ast.statement.BoxWhile; import ortus.boxlang.compiler.ast.statement.component.BoxComponent; +import ortus.boxlang.compiler.ast.statement.component.BoxTemplateIsland; import ortus.boxlang.compiler.parser.BoxSourceType; import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; -import ortus.boxlang.runtime.dynamic.IReferenceable; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; -import ortus.boxlang.runtime.dynamic.javaproxy.InterfaceProxyService; import ortus.boxlang.runtime.loader.ImportDefinition; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ClassVariablesScope; import ortus.boxlang.runtime.scopes.Key; -import ortus.boxlang.runtime.scopes.StaticScope; -import ortus.boxlang.runtime.scopes.ThisScope; -import ortus.boxlang.runtime.scopes.VariablesScope; -import ortus.boxlang.runtime.types.*; +import ortus.boxlang.runtime.types.AbstractFunction; +import ortus.boxlang.runtime.types.Argument; +import ortus.boxlang.runtime.types.DefaultExpression; +import ortus.boxlang.runtime.types.Function; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.Property; +import ortus.boxlang.runtime.types.Struct; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.types.exceptions.ExpressionException; -import ortus.boxlang.runtime.types.meta.BoxMeta; -import ortus.boxlang.runtime.types.util.BLCollector; -import ortus.boxlang.runtime.types.util.ListUtil; import ortus.boxlang.runtime.types.util.MapHelper; import ortus.boxlang.runtime.util.ResolvedFilePath; @@ -198,11 +208,20 @@ public AsmTranspiler() { registry.put( BoxForIndex.class, new BoxForIndexTransformer( this ) ); registry.put( BoxClosure.class, new BoxClosureTransformer( this ) ); registry.put( BoxComponent.class, new BoxComponentTransformer( this ) ); + registry.put( BoxStaticInitializer.class, new BoxStaticInitializerTransformer( this ) ); + registry.put( BoxStaticAccess.class, new BoxStaticAccessTransformer( this ) ); + registry.put( BoxStaticMethodInvocation.class, new BoxStaticMethodInvocationTransformer( this ) ); + registry.put( BoxScriptIsland.class, new BoxScriptIslandTransformer( this ) ); + registry.put( BoxTemplateIsland.class, new BoxTemplateIslandTransformer( this ) ); + registry.put( BoxExpressionInvocation.class, new BoxExpressionInvocationTransformer( this ) ); + registry.put( BoxParam.class, new BoxParamTransformer( this ) ); } @Override public ClassNode transpile( BoxScript boxScript ) throws BoxRuntimeException { - Type type = Type.getType( "L" + getProperty( "packageName" ).replace( '.', '/' ) + "/" + getProperty( "classname" ) + ";" ); + Type type = Type.getType( "L" + getProperty( "packageName" ).replace( '.', '/' ) + "/" + getProperty( "classname" ) + ";" ); + setProperty( "classType", type.getDescriptor() ); + setProperty( "classTypeInternal", type.getInternalName() ); ClassNode classNode = new ClassNode(); String mappingName = getProperty( "mappingName" ); String mappingPath = getProperty( "mappingPath" ); @@ -324,430 +343,11 @@ public ClassNode transpile( BoxScript boxScript ) throws BoxRuntimeException { @Override public ClassNode transpile( BoxClass boxClass ) throws BoxRuntimeException { - Source source = boxClass.getPosition().getSource(); - String sourceType = getProperty( "sourceType" ); - - String filePath = source instanceof SourceFile file && file.getFile() != null ? file.getFile().getAbsolutePath() - : "unknown"; - String boxClassName = getProperty( "boxFQN" ); - String mappingName = getProperty( "mappingName" ); - String mappingPath = getProperty( "mappingPath" ); - String relativePath = getProperty( "relativePath" ); - - Type type = Type.getType( "L" + getProperty( "packageName" ).replace( '.', '/' ) - + "/" + getProperty( "classname" ) + ";" ); - - List interfaces = new ArrayList<>(); - interfaces.add( Type.getType( IClassRunnable.class ) ); - interfaces.add( Type.getType( IReferenceable.class ) ); - interfaces.add( Type.getType( IType.class ) ); - interfaces.add( Type.getType( Serializable.class ) ); - List interfaceMethods = List.of(); - BoxExpression implementsValue = boxClass.getAnnotations().stream() - .filter( it -> it.getKey().getValue().equalsIgnoreCase( "implements" ) ) - .findFirst() - .map( BoxAnnotation::getValue ) - .orElse( null ); - if ( implementsValue instanceof BoxStringLiteral str ) { - String implementsStringList = str.getValue(); - // Collect and trim all strings starting with "java:" - Array implementsArray = ListUtil.asList( implementsStringList, "," ).stream() - .map( String::valueOf ) - .map( String::trim ) - .filter( it -> it.toLowerCase().startsWith( "java:" ) ) - .map( it -> it.substring( 5 ) ) - .collect( BLCollector.toArray() ); - var interfaceProxyDefinition = InterfaceProxyService.generateDefinition( new ScriptingRequestBoxContext(), implementsArray ); - // TODO: Remove methods that already have a @overrideJava UDF definition to avoid duplicates - interfaces.addAll( interfaceProxyDefinition.interfaces().stream().map( iface -> Type.getType( "L" + iface.replace( '.', '/' ) + ";" ) ).toList() ); - interfaceMethods = interfaceProxyDefinition.methods().stream() - .map( method -> AsmHelper.dereferenceAndInvoke( method.getName(), Type.getType( method ), type ) ) - .toList(); - } - - Type superclass = Type.getType( Object.class ); - boolean isJavaExtends; - List extendsMethods = List.of(); - BoxExpression extendsValue = boxClass.getAnnotations().stream() - .filter( it -> it.getKey().getValue().equalsIgnoreCase( "extends" ) ) - .findFirst() - .map( BoxAnnotation::getValue ) - .orElse( null ); - if ( extendsValue instanceof BoxStringLiteral str ) { - String extendsStringValue = str.getValue().trim(); - if ( extendsStringValue.toLowerCase().startsWith( "java:" ) ) { - superclass = Type.getType( "L" + extendsStringValue.substring( 5 ).replace( '.', '/' ) + ";" ); - isJavaExtends = true; - // search for UDFs that need a proxy created - extendsMethods = boxClass.getDescendantsOfType( BoxFunctionDeclaration.class ) - .stream() - .filter( it -> it.getAnnotations().stream().anyMatch( anno -> anno.getKey().getValue().equalsIgnoreCase( EXTENDS_ANNOTATION_MARKER ) ) ) - .map( func -> { - BoxReturnType boxReturnType = func.getType(); - BoxType boxType = BoxType.Any; - String fqn = null; - if ( boxReturnType != null ) { - boxType = boxReturnType.getType(); - if ( boxType.equals( BoxType.Fqn ) ) { - fqn = boxReturnType.getFqn(); - } - } - Type returnType = Type - .getType( "L" + ( boxType.equals( BoxType.Fqn ) ? fqn : boxType.getSymbol() ).replace( '.', '/' ) + ";" ); - List parameters = func.getArgs(); - Type[] parameterTypes = new Type[ parameters.size() ]; - for ( int i = 0; i < parameters.size(); i++ ) { - BoxArgumentDeclaration parameter = parameters.get( i ); - parameterTypes[ i ] = Type.getType( "L" + parameter.getType().replace( '.', '/' ) + ";" ); - - } - return AsmHelper.dereferenceAndInvoke( func.getName(), Type.getMethodType( returnType, parameterTypes ), type ); - } ) - .toList(); - } else { - isJavaExtends = false; - } - } else { - isJavaExtends = false; - } - - ClassNode classNode = new ClassNode(); - - AsmHelper.init( classNode, false, type, superclass, methodVisitor -> { - methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); - methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ClassVariablesScope.class ) ); - methodVisitor.visitInsn( Opcodes.DUP ); - methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); - methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, - Type.getInternalName( ClassVariablesScope.class ), - "", - Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IClassRunnable.class ) ), - false ); - methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "variablesScope", Type.getDescriptor( VariablesScope.class ) ); - - methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); - methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ThisScope.class ) ); - methodVisitor.visitInsn( Opcodes.DUP ); - methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, - Type.getInternalName( ThisScope.class ), - "", - Type.getMethodDescriptor( Type.VOID_TYPE ), - false ); - methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "thisScope", Type.getDescriptor( ThisScope.class ) ); - - methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); - createKey( boxClassName ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "name", Type.getDescriptor( Key.class ) ); - - methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); - methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ArrayList.class ) ); - methodVisitor.visitInsn( Opcodes.DUP ); - methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, Type.getInternalName( ArrayList.class ), "", Type.getMethodDescriptor( Type.VOID_TYPE ), - false ); - methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "interfaces", Type.getDescriptor( List.class ) ); - }, interfaces.toArray( Type[]::new ) ); - - interfaceMethods.forEach( methodNode -> methodNode.accept( classNode ) ); - extendsMethods.forEach( methodNode -> methodNode.accept( classNode ) ); - - classNode.visitField( Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, "serialVersionUID", Type.getDescriptor( long.class ), null, 1L ) - .visitEnd(); - - AsmHelper.addStaticFieldGetter( classNode, - type, - "imports", - "getImports", - Type.getType( List.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "path", - "getRunnablePath", - Type.getType( ResolvedFilePath.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "sourceType", - "getSourceType", - Type.getType( BoxSourceType.class ), - null ); - AsmHelper.addStaticFieldGetterWithStaticGetter( classNode, - type, - "annotations", - "getAnnotations", - "getAnnotationsStatic", - Type.getType( IStruct.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "documentation", - "getDocumentation", - Type.getType( IStruct.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "properties", - "getProperties", - Type.getType( Map.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "getterLookup", - "getGetterLookup", - Type.getType( Map.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "setterLookup", - "getSetterLookup", - Type.getType( Map.class ), - null ); - AsmHelper.addStaticFieldGetter( classNode, - type, - "isJavaExtends", - "isJavaExtends", - Type.BOOLEAN_TYPE, - isJavaExtends ? 1 : 0 ); - AsmHelper.addStaticFieldGetterWithStaticGetter( classNode, - type, - "staticScope", - "getStaticScope", - "getStaticScopeStatic", - Type.getType( StaticScope.class ), - null ); - - classNode.visitField( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, - "keys", - Type.getDescriptor( Key[].class ), - null, - null ).visitEnd(); - classNode.visitField( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, - "staticInitialized", - Type.getDescriptor( boolean.class ), - null, - 0 ).visitEnd(); - - AsmHelper.addFieldGetter( classNode, - type, - "variablesScope", - "getVariablesScope", - Type.getType( VariablesScope.class ), - null ); - AsmHelper.addFieldGetter( classNode, - type, - "thisScope", - "getThisScope", - Type.getType( ThisScope.class ), - null ); - AsmHelper.addFieldGetter( classNode, - type, - "name", - "getName", - Type.getType( Key.class ), - null ); - AsmHelper.addFieldGetter( classNode, - type, - "interfaces", - "getInterfaces", - Type.getType( List.class ), - null ); - AsmHelper.addFieldGetterAndSetter( classNode, - type, - "_super", - "getSuper", - "_setSuper", - Type.getType( IClassRunnable.class ), - null, - methodVisitor -> { - } ); - AsmHelper.addFieldGetterAndSetter( classNode, - type, - "child", - "getChild", - "setChild", - Type.getType( IClassRunnable.class ), - null, - methodVisitor -> { - } ); - AsmHelper.addFieldGetterAndSetter( classNode, - type, - "canOutput", - "getCanOutput", - "setCanOutput", - Type.getType( Boolean.class ), - null, - methodVisitor -> { - } ); - AsmHelper.addFieldGetterAndSetter( classNode, - type, - "$bx", - "_getbx", - "_setbx", - Type.getType( BoxMeta.class ), - null, - methodVisitor -> { - } ); - AsmHelper.addFieldGetterAndSetter( classNode, - type, - "canInvokeImplicitAccessor", - "getCanInvokeImplicitAccessor", - "setCanInvokeImplicitAccessor", - Type.getType( Boolean.class ), - null, - methodVisitor -> { - } ); - - AsmHelper.boxClassSupport( classNode, "pseudoConstructor", Type.VOID_TYPE, Type.getType( IBoxContext.class ) ); - AsmHelper.boxClassSupport( classNode, "canOutput", Type.getType( Boolean.class ) ); - AsmHelper.boxClassSupport( classNode, "getBoxMeta", Type.getType( BoxMeta.class ) ); - AsmHelper.boxClassSupport( classNode, "getMetaData", Type.getType( IStruct.class ) ); - AsmHelper.boxClassSupport( classNode, "asString", Type.getType( String.class ) ); - AsmHelper.boxClassSupport( classNode, "canInvokeImplicitAccessor", Type.getType( Boolean.class ), Type.getType( IBoxContext.class ) ); - AsmHelper.boxClassSupport( classNode, "setSuper", Type.VOID_TYPE, Type.getType( IClassRunnable.class ) ); - AsmHelper.boxClassSupport( classNode, "getBottomClass", Type.getType( IClassRunnable.class ) ); - AsmHelper.boxClassSupport( classNode, "assign", Type.getType( Object.class ), Type.getType( IBoxContext.class ), Type.getType( Key.class ), - Type.getType( Object.class ) ); - AsmHelper.boxClassSupport( classNode, "dereference", Type.getType( Object.class ), Type.getType( IBoxContext.class ), Type.getType( Key.class ), - Type.getType( Boolean.class ) ); - AsmHelper.boxClassSupport( classNode, "dereferenceAndInvoke", Type.getType( Object.class ), Type.getType( IBoxContext.class ), - Type.getType( Key.class ), Type.getType( Object[].class ), Type.getType( Boolean.class ) ); - AsmHelper.boxClassSupport( classNode, "dereferenceAndInvoke", Type.getType( Object.class ), Type.getType( IBoxContext.class ), - Type.getType( Key.class ), Type.getType( Map.class ), Type.getType( Boolean.class ) ); - AsmHelper.boxClassSupport( classNode, "registerInterface", Type.VOID_TYPE, Type.getType( BoxInterface.class ) ); - - AsmHelper.methodWithContextAndClassLocator( classNode, "_pseudoConstructor", Type.getType( IBoxContext.class ), Type.VOID_TYPE, false, this, true, - () -> boxClass.getBody().stream().flatMap( statement -> transform( statement, TransformerContext.NONE, ReturnValueContext.EMPTY ).stream() ) - .toList() - ); - - AsmHelper.methodWithContextAndClassLocator( classNode, "staticInitializer", Type.getType( IBoxContext.class ), Type.VOID_TYPE, true, this, true, - List::of - ); - - AsmHelper.complete( classNode, type, methodVisitor -> { - AsmHelper.resolvedFilePath( methodVisitor, mappingName, mappingPath, relativePath, filePath ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "path", - Type.getDescriptor( ResolvedFilePath.class ) ); - - methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, - Type.getInternalName( BoxSourceType.class ), - sourceType, - Type.getDescriptor( BoxSourceType.class ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "sourceType", - Type.getDescriptor( BoxSourceType.class ) ); - - List> imports = new ArrayList<>(); - for ( BoxImport statement : boxClass.getImports() ) { - imports.add( transform( statement, TransformerContext.NONE, ReturnValueContext.EMPTY ) ); - } - List annotations = transformAnnotations( boxClass.getAnnotations() ); - List> properties = transformProperties( type, boxClass.getProperties(), sourceType ); - - methodVisitor.visitLdcInsn( getKeys().size() ); - methodVisitor.visitTypeInsn( Opcodes.ANEWARRAY, Type.getInternalName( Key.class ) ); - int index = 0; - for ( BoxExpression expression : getKeys().values() ) { - methodVisitor.visitInsn( Opcodes.DUP ); - methodVisitor.visitLdcInsn( index++ ); - transform( expression, TransformerContext.NONE, ReturnValueContext.EMPTY ).forEach( methodInsnNode -> methodInsnNode.accept( methodVisitor ) ); - methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, - Type.getInternalName( Key.class ), - "of", - Type.getMethodDescriptor( Type.getType( Key.class ), Type.getType( Object.class ) ), - false ); - methodVisitor.visitInsn( Opcodes.AASTORE ); - } - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "keys", - Type.getDescriptor( Key[].class ) ); - - methodVisitor.visitLdcInsn( 0 ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "staticInitialized", - Type.getDescriptor( boolean.class ) ); - - methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( StaticScope.class ) ); - methodVisitor.visitInsn( Opcodes.DUP ); - methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, - Type.getInternalName( StaticScope.class ), - "", - Type.getMethodDescriptor( Type.VOID_TYPE ), - false ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "staticScope", - Type.getDescriptor( StaticScope.class ) ); - - methodVisitor.visitLdcInsn( isJavaExtends ? 1 : 0 ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "isJavaExtends", Type.getDescriptor( boolean.class ) ); - - methodVisitor.visitLdcInsn( 1L ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "serialVersionUID", Type.getDescriptor( long.class ) ); - - AsmHelper.array( Type.getType( ImportDefinition.class ), Stream.concat( - imports.stream(), - getImports().stream().map( raw -> { - List nodes = new ArrayList<>(); - nodes.addAll( raw ); - nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, - Type.getInternalName( ImportDefinition.class ), - "parse", - Type.getMethodDescriptor( Type.getType( ImportDefinition.class ), Type.getType( String.class ) ), - false ) ); - return nodes; - } ) - ).toList() ).forEach( node -> node.accept( methodVisitor ) ); - methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, - Type.getInternalName( List.class ), - "of", - Type.getMethodDescriptor( Type.getType( List.class ), Type.getType( Object[].class ) ), - true ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "imports", - Type.getDescriptor( List.class ) ); - - annotations.forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "annotations", - Type.getDescriptor( IStruct.class ) ); - - methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, - Type.getInternalName( Struct.class ), - "EMPTY", - Type.getDescriptor( IStruct.class ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "documentation", - Type.getDescriptor( IStruct.class ) ); - - properties.get( 0 ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "properties", - Type.getDescriptor( Map.class ) ); - - properties.get( 1 ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "getterLookup", - Type.getDescriptor( Map.class ) ); - - properties.get( 2 ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); - methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, - type.getInternalName(), - "setterLookup", - Type.getDescriptor( Map.class ) ); - } ); + return BoxClassTransformer.transpile( this, boxClass ); + } - return classNode; + public ClassNode transpile( BoxInterface boxClass ) throws BoxRuntimeException { + return BoxInterfaceTransformer.transpile( this, boxClass ); } @Override @@ -759,7 +359,8 @@ public List transform( BoxNode node, TransformerContext contex throw new IllegalStateException( "unsupported: " + node.getClass().getSimpleName() + " : " + node.getSourceText() ); } - private List> transformProperties( Type declaringType, List properties, String sourceType ) { + @Override + public List> transformProperties( Type declaringType, List properties, String sourceType ) { List> members = new ArrayList<>(); List> getterLookup = new ArrayList<>(); List> setterLookup = new ArrayList<>(); @@ -769,78 +370,30 @@ private List> transformProperties( Type declaringType, Li * normalize annotations to allow for * property String userName; */ - List finalAnnotations = new ArrayList(); + List finalAnnotations = normlizePropertyAnnotations( prop ); // Start wiith all inline annotatinos - var annotations = prop.getPostAnnotations(); - // Add in any pre annotations that have a value, which allows type, name, or default to be set before - annotations.addAll( prop.getAnnotations().stream().filter( it -> it.getValue() != null ).toList() ); - int namePosition = annotations.stream().filter( it -> it.getValue() != null ).map( BoxAnnotation::getKey ) - .map( BoxFQN::getValue ).map( String::toLowerCase ) - .collect( java.util.stream.Collectors.toList() ).indexOf( "name" ); - int typePosition = annotations.stream().filter( it -> it.getValue() != null ).map( BoxAnnotation::getKey ) - .map( BoxFQN::getValue ).map( String::toLowerCase ) - .collect( java.util.stream.Collectors.toList() ).indexOf( "type" ); - int defaultPosition = annotations.stream().filter( it -> it.getValue() != null ).map( BoxAnnotation::getKey ) - .map( BoxFQN::getValue ).map( String::toLowerCase ) - .collect( java.util.stream.Collectors.toList() ).indexOf( "default" ); - int numberOfNonValuedKeys = ( int ) annotations.stream().map( BoxAnnotation::getValue ).filter( it -> it == null ).count(); - List nonValuedKeys = annotations.stream().filter( it -> it.getValue() == null ) - .collect( java.util.stream.Collectors.toList() ); - BoxAnnotation nameAnnotation = null; - BoxAnnotation typeAnnotation = null; - BoxAnnotation defaultAnnotation = null; - - if ( namePosition > -1 ) - nameAnnotation = annotations.get( namePosition ); - if ( typePosition > -1 ) - typeAnnotation = annotations.get( typePosition ); - if ( defaultPosition > -1 ) - defaultAnnotation = annotations.get( defaultPosition ); - /* - * If there is no name, if there is more than one nonvalued keys and no type, use the first nonvalued key - * as the type and second nonvalued key as the name. Otherwise, if there are more than one non-valued key, use the first as the name. - */ - if ( namePosition == -1 ) { - if ( numberOfNonValuedKeys > 1 && typePosition == -1 ) { - typeAnnotation = new BoxAnnotation( new BoxFQN( "type", null, null ), - new BoxStringLiteral( nonValuedKeys.get( 0 ).getKey().getValue(), null, null ), null, - null ); - nameAnnotation = new BoxAnnotation( new BoxFQN( "name", null, null ), - new BoxStringLiteral( nonValuedKeys.get( 1 ).getKey().getValue(), null, null ), null, - null ); - finalAnnotations.add( nameAnnotation ); - finalAnnotations.add( typeAnnotation ); - annotations.remove( nonValuedKeys.get( 0 ) ); - annotations.remove( nonValuedKeys.get( 1 ) ); - } else if ( numberOfNonValuedKeys > 0 ) { - nameAnnotation = new BoxAnnotation( new BoxFQN( "name", null, null ), - new BoxStringLiteral( nonValuedKeys.get( 0 ).getKey().getValue(), null, null ), null, - null ); - finalAnnotations.add( nameAnnotation ); - annotations.remove( nonValuedKeys.get( 0 ) ); - } else { - throw new ExpressionException( "Property [" + prop.getSourceText() + "] has no name", prop ); - } - } - // add type with value of any if not present - if ( typeAnnotation == null ) { - typeAnnotation = new BoxAnnotation( new BoxFQN( "type", null, null ), new BoxStringLiteral( "any", null, null ), null, - null ); - finalAnnotations.add( typeAnnotation ); - } - // add default with value of null if not present - if ( defaultPosition == -1 ) { - defaultAnnotation = new BoxAnnotation( new BoxFQN( "default", null, null ), new BoxNull( null, null ), null, - null ); - finalAnnotations.add( defaultAnnotation ); - } - // add remaining annotations - finalAnnotations.addAll( annotations ); - // Now that name, type, and default are finalized, add in any remaining non-valued keys - finalAnnotations.addAll( prop.getAnnotations().stream().filter( it -> it.getValue() == null ).toList() ); + + BoxAnnotation nameAnnotation = finalAnnotations.stream().filter( it -> it.getKey().getValue().equalsIgnoreCase( "name" ) ) + .findFirst() + .orElseThrow( () -> new ExpressionException( "Property [" + prop.getSourceText() + "] missing name annotation", prop ) ); + BoxAnnotation typeAnnotation = finalAnnotations.stream().filter( it -> it.getKey().getValue().equalsIgnoreCase( "type" ) ) + .findFirst() + .orElseThrow( () -> new ExpressionException( "Property [" + prop.getSourceText() + "] missing type annotation", prop ) ); + BoxAnnotation defaultAnnotation = finalAnnotations.stream().filter( it -> it.getKey().getValue().equalsIgnoreCase( "default" ) ) + .findFirst() + .orElse( null ); + + // List defaultLiteral = List.of( new InsnNode( Opcodes.ACONST_NULL ) ); + // List defaultExpression = List.of( new InsnNode( Opcodes.ACONST_NULL ) ); + // if ( defaultAnnotation.getValue() != null ) { + // if ( defaultAnnotation.getValue().isLiteral() ) { + // defaultLiteral = transform( defaultAnnotation.getValue(), TransformerContext.NONE ); + // } else { + // defaultExpression = AsmHelper.getDefaultExpression( this, defaultAnnotation.getValue() ); + // } + // } List annotationStruct = transformAnnotations( finalAnnotations ); - /* Process default value */ List init, initLambda; if ( defaultAnnotation.getValue() != null ) { @@ -872,17 +425,18 @@ private List> transformProperties( Type declaringType, Li init = List.of( new InsnNode( Opcodes.ACONST_NULL ) ); initLambda = List.of( new InsnNode( Opcodes.ACONST_NULL ) ); } + + String type; // name and type must be simple values String name; - String type; - if ( nameAnnotation.getValue() instanceof BoxStringLiteral namelit ) { + if ( nameAnnotation != null && nameAnnotation.getValue() instanceof BoxStringLiteral namelit ) { name = namelit.getValue().trim(); if ( name.isEmpty() ) throw new ExpressionException( "Property [" + prop.getSourceText() + "] name cannot be empty", nameAnnotation ); } else { throw new ExpressionException( "Property [" + prop.getSourceText() + "] name must be a simple value", nameAnnotation ); } - if ( typeAnnotation.getValue() instanceof BoxStringLiteral typelit ) { + if ( typeAnnotation != null && typeAnnotation.getValue() instanceof BoxStringLiteral typelit ) { type = typelit.getValue().trim(); if ( type.isEmpty() ) throw new ExpressionException( "Property [" + prop.getSourceText() + "] type cannot be empty", typeAnnotation ); @@ -988,6 +542,102 @@ private List> transformProperties( Type declaringType, Li } } + public static List normlizePropertyAnnotations( BoxProperty prop ) { + + /** + * normalize annotations to allow for + * property String userName; + * This means all inline and pre annotations are treated as post annotations + */ + List finalAnnotations = new ArrayList<>(); + // Start wiith all inline annotatinos + List annotations = prop.getPostAnnotations(); + // Add in any pre annotations that have a value, which allows type, name, or default to be set before + annotations.addAll( prop.getAnnotations().stream().filter( it -> it.getValue() != null ).toList() ); + + // Find the position of the name, type, and default annotations + int namePosition = annotations.stream() + .filter( it -> it.getKey().getValue().equalsIgnoreCase( "name" ) && it.getValue() != null ) + .findFirst() + .map( annotations::indexOf ).orElse( -1 ); + int typePosition = annotations.stream() + .filter( it -> it.getKey().getValue().equalsIgnoreCase( "type" ) && it.getValue() != null ) + .findFirst() + .map( annotations::indexOf ).orElse( -1 ); + int defaultPosition = annotations.stream() + .filter( it -> it.getKey().getValue().equalsIgnoreCase( "default" ) && it.getValue() != null ) + .findFirst() + .map( annotations::indexOf ).orElse( -1 ); + + // Count the number of non-valued keys to determine how to handle them by position later + int numberOfNonValuedKeys = ( int ) annotations.stream() + .map( BoxAnnotation::getValue ) + .filter( Objects::isNull ) + .count(); + List nonValuedKeys = annotations.stream() + .filter( it -> it.getValue() == null ) + .collect( java.util.stream.Collectors.toList() ); + + // Find the name, type, and default annotations + BoxAnnotation nameAnnotation = null; + BoxAnnotation typeAnnotation = null; + BoxAnnotation defaultAnnotation = null; + if ( namePosition > -1 ) + nameAnnotation = annotations.get( namePosition ); + if ( typePosition > -1 ) + typeAnnotation = annotations.get( typePosition ); + if ( defaultPosition > -1 ) + defaultAnnotation = annotations.get( defaultPosition ); + + /** + * If there is no name, if there is more than one nonvalued keys and no type, use the first nonvalued key + * as the type and second nonvalued key as the name. Otherwise, if there are more than one non-valued key, use the first as the name. + */ + if ( namePosition == -1 ) { + if ( numberOfNonValuedKeys > 1 && typePosition == -1 ) { + typeAnnotation = new BoxAnnotation( new BoxFQN( "type", null, null ), + new BoxStringLiteral( nonValuedKeys.get( 0 ).getKey().getValue(), null, null ), null, + null ); + nameAnnotation = new BoxAnnotation( new BoxFQN( "name", null, null ), + new BoxStringLiteral( nonValuedKeys.get( 1 ).getKey().getValue(), null, null ), null, + null ); + finalAnnotations.add( nameAnnotation ); + finalAnnotations.add( typeAnnotation ); + annotations.remove( nonValuedKeys.get( 0 ) ); + annotations.remove( nonValuedKeys.get( 1 ) ); + } else if ( numberOfNonValuedKeys > 0 ) { + nameAnnotation = new BoxAnnotation( new BoxFQN( "name", null, null ), + new BoxStringLiteral( nonValuedKeys.get( 0 ).getKey().getValue(), null, null ), null, + null ); + finalAnnotations.add( nameAnnotation ); + annotations.remove( nonValuedKeys.get( 0 ) ); + } else { + throw new ExpressionException( "Property [" + prop.getSourceText() + "] has no name", prop ); + } + } + + // add type with value of any if not present + if ( typeAnnotation == null ) { + typeAnnotation = new BoxAnnotation( new BoxFQN( "type", null, null ), new BoxStringLiteral( "any", null, null ), null, + null ); + finalAnnotations.add( typeAnnotation ); + } + + // add default with value of null if not present + if ( defaultPosition == -1 ) { + defaultAnnotation = new BoxAnnotation( new BoxFQN( "default", null, null ), new BoxNull( null, null ), null, + null ); + finalAnnotations.add( defaultAnnotation ); + } + + // add remaining annotations + finalAnnotations.addAll( annotations ); + // Now that name, type, and default are finalized, add in any remaining non-valued keys + finalAnnotations.addAll( prop.getAnnotations().stream().filter( it -> it.getValue() == null ).toList() ); + + return finalAnnotations; + } + private static String getBoxExprAsString( BoxExpression expr ) { if ( expr == null ) { return ""; @@ -1001,4 +651,124 @@ private static String getBoxExprAsString( BoxExpression expr ) { throw new BoxRuntimeException( "Unsupported BoxExpr type: " + expr.getClass().getSimpleName() ); } } + + public List createAbstractFunction( BoxFunctionDeclaration func ) { + List nodes = new ArrayList(); + + // public AbstractFunction( Key name, Argument[] arguments, String returnType, Access access, IStruct annotations, IStruct documentation, + // String sourceObjectName, String sourceObjectType ) { + // this.name = name; + // this.arguments = arguments; + // this.returnType = returnType; + // this.access = access; + // this.annotations = annotations; + // this.documentation = documentation; + // this.sourceObjectName = sourceObjectName; + // this.sourceObjectType = sourceObjectType; + // } + + nodes.add( new TypeInsnNode( Opcodes.NEW, Type.getInternalName( AbstractFunction.class ) ) ); + nodes.add( new InsnNode( Opcodes.DUP ) ); + + // args + // Key name + nodes.addAll( createKey( func.getName() ) ); + // Argument[] arguments + List> argList = func.getArgs() + .stream() + .map( arg -> transform( arg, TransformerContext.NONE ) ) + .toList(); + nodes.addAll( AsmHelper.array( Type.getType( Argument.class ), argList ) ); + // String returnType + nodes.addAll( transform( func.getType(), TransformerContext.NONE ) ); + // Access access + nodes.add( + new FieldInsnNode( + Opcodes.GETSTATIC, + Type.getDescriptor( Function.Access.class ), + func.getAccessModifier().name().toUpperCase(), + Type.getDescriptor( Function.Access.class ) + ) + ); + // IStruct annotations + // TODO + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, + Type.getInternalName( Struct.class ), + "EMPTY", + Type.getDescriptor( IStruct.class ) ) ); + // IStruct documentation + // TODO + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, + Type.getInternalName( Struct.class ), + "EMPTY", + Type.getDescriptor( IStruct.class ) ) ); + // String sourceObjectName + nodes.add( new LdcInsnNode( getProperty( "boxClassName" ) ) ); + // String sourceObjectType + nodes.add( new LdcInsnNode( "class" ) ); + + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESPECIAL, + Type.getInternalName( ClassVariablesScope.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IClassRunnable.class ) ), + false + ) + ); + + return nodes; + } + + private List generateMapOfAbstractMethodNames( BoxClass boxClass ) { + List> methodKeyLists = boxClass.getDescendantsOfType( BoxFunctionDeclaration.class ) + .stream() + .filter( func -> func.getBody() == null ) + .map( func -> { + List> absFunc = List.of( + createKey( func.getName() ), + createAbstractFunction( func ) + ); + + return absFunc; + } ) + .flatMap( x -> x.stream() ) + .collect( java.util.stream.Collectors.toList() ); + + List nodes = new ArrayList(); + + nodes.addAll( AsmHelper.array( Type.getType( Key.class ), methodKeyLists ) ); + + nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( MapHelper.class ), + "LinkedHashMapOfProperties", + Type.getMethodDescriptor( Type.getType( Map.class ), Type.getType( Object[].class ) ), + false ) ); + + return nodes; + } + + private List generateSetOfCompileTimeMethodNames( BoxClass boxClass ) { + List> methodKeyLists = boxClass.getDescendantsOfType( BoxFunctionDeclaration.class ) + .stream() + .map( BoxFunctionDeclaration::getName ) + .map( this::createKey ) + .collect( java.util.stream.Collectors.toList() ); + + List nodes = new ArrayList(); + + nodes.addAll( AsmHelper.array( Type.getType( Key.class ), methodKeyLists ) ); + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Set.class ), + "of", + Type.getMethodDescriptor( Type.getType( Set.class ), Type.getType( Object[].class ) ), + true + ) + ); + + return nodes; + + } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/MethodContextTracker.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/MethodContextTracker.java index 5bf6eba5e..f75a8c003 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/MethodContextTracker.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/MethodContextTracker.java @@ -1,30 +1,118 @@ package ortus.boxlang.compiler.asmboxpiler; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.IntStream; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.TryCatchBlockNode; import org.objectweb.asm.tree.VarInsnNode; +import ortus.boxlang.compiler.ast.BoxNode; + public class MethodContextTracker { - private int varCount = 0; - private int unusedStackEntries = 0; - private List contextStack = new ArrayList(); + private int varCount = 0; + private int unusedStackEntries = 0; + private List contextStack = new ArrayList(); + private List tryCatchBlockNodes = new ArrayList(); + private Map breaks = new LinkedHashMap<>(); + private Map continues = new LinkedHashMap<>(); + private final CompilationType type; + private Map nodeBreaks = new LinkedHashMap<>(); + private Map nodeContinues = new LinkedHashMap<>(); + private Map stringLabel = new LinkedHashMap<>(); + + public enum CompilationType { + BoxClass, + Component, + Function + } public record VarStore( int index, List nodes ) { } - public int getUnusedStackCount() { - return unusedStackEntries; + public MethodContextTracker( boolean isStatic ) { + this( CompilationType.BoxClass, isStatic ); } - public MethodContextTracker( boolean isStatic ) { - varCount = isStatic ? -1 : 0; + public MethodContextTracker( CompilationType type, boolean isStatic ) { + this.type = type; + varCount = isStatic ? -1 : 0; + } + + public boolean canReturn() { + return this.type == CompilationType.Function; + } + + public void setStringLabel( String label, BoxNode target ) { + stringLabel.put( label, target ); + } + + public BoxNode getStringLabel( String label ) { + return stringLabel.get( label ); + } + + public void setBreak( BoxNode node, LabelNode label ) { + nodeBreaks.put( node, label ); + } + + public LabelNode getBreak( BoxNode node ) { + return nodeBreaks.get( node ); + } + + public void setContinue( BoxNode node, LabelNode label ) { + nodeContinues.put( node, label ); + } + + public LabelNode getContinue( BoxNode node ) { + return nodeContinues.get( node ); + } + + public LabelNode getCurrentBreak( String label ) { + return breaks.get( label == null ? "" : label ); + } + + public void setCurrentBreak( String label, LabelNode labelNode ) { + this.breaks.put( label == null ? "" : label, labelNode ); + } + + public void removeCurrentBreak( String label ) { + this.breaks.remove( label == null ? "" : label ); + } + + public LabelNode getCurrentContinue( String label ) { + return continues.get( label == null ? "" : label ); + } + + public void setCurrentContinue( String label, LabelNode labelNode ) { + this.continues.put( label == null ? "" : label, labelNode ); + } + + public void removeCurrentContinue( String label ) { + this.continues.remove( label == null ? "" : label ); + } + + public List getTryCatchStack() { + return tryCatchBlockNodes; + } + + public void addTryCatchBlock( TryCatchBlockNode tryCatchBlockNode ) { + tryCatchBlockNodes.add( tryCatchBlockNode ); + } + + public void clearTryCatchStack() { + tryCatchBlockNodes = new ArrayList(); + } + + public int getUnusedStackCount() { + return unusedStackEntries; } public void trackUnusedStackEntry() { diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/Transpiler.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/Transpiler.java index a7f8711dd..f7fedd2da 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/Transpiler.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/Transpiler.java @@ -21,17 +21,22 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.statement.BoxInterfaceTransformer; import ortus.boxlang.compiler.ast.BoxExpression; +import ortus.boxlang.compiler.ast.BoxInterface; import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.BoxStaticInitializer; import ortus.boxlang.compiler.ast.expression.BoxIdentifier; import ortus.boxlang.compiler.ast.expression.BoxIntegerLiteral; import ortus.boxlang.compiler.ast.expression.BoxStringLiteral; import ortus.boxlang.compiler.ast.statement.BoxAnnotation; import ortus.boxlang.compiler.ast.statement.BoxDocumentationAnnotation; +import ortus.boxlang.compiler.ast.statement.BoxProperty; import ortus.boxlang.runtime.loader.ImportDefinition; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.IStruct; import ortus.boxlang.runtime.types.Struct; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; public abstract class Transpiler implements ITranspiler { @@ -40,10 +45,13 @@ public abstract class Transpiler implements ITranspiler { private Map auxiliaries = new LinkedHashMap(); private List tryCatchBlockNodes = new ArrayList(); private int lambdaCounter = 0; + private int componentCounter = 0; + private int functionBodyCounter = 0; private Map breaks = new LinkedHashMap<>(); private Map continues = new LinkedHashMap<>(); private List imports = new ArrayList<>(); private List methodContextTrackers = new ArrayList(); + private List staticInitializers = new ArrayList<>(); /** * Set a property @@ -55,6 +63,42 @@ public void setProperty( String key, String value ) { properties.put( key, value ); } + public boolean canReturn() { + return functionBodyCounter > 0; + } + + public void incrementfunctionBodyCounter() { + functionBodyCounter++; + } + + public void decrementfunctionBodyCounter() { + functionBodyCounter--; + } + + public boolean isInsideComponent() { + return componentCounter > 0; + } + + public int getComponentCounter() { + return componentCounter; + } + + public void setComponentCounter( int counter ) { + componentCounter = counter; + } + + public void incrementComponentCounter() { + componentCounter++; + } + + public void decrementComponentCounter() { + componentCounter--; + } + + public ClassNode transpile( BoxInterface boxClass ) throws BoxRuntimeException { + return BoxInterfaceTransformer.transpile( this, boxClass ); + } + /** * Get a Propoerty * @@ -123,6 +167,14 @@ public void clearTryCatchStack() { tryCatchBlockNodes = new ArrayList(); } + public void addBoxStaticInitializer( BoxStaticInitializer staticInitializer ) { + this.staticInitializers.add( staticInitializer ); + } + + public List getBoxStaticInitializers() { + return this.staticInitializers; + } + public Map getAuxiliary() { return auxiliaries; } @@ -163,6 +215,8 @@ public List createKey( BoxExpression expr ) { } } + public abstract List> transformProperties( Type declaringType, List properties, String sourceType ); + public List createKey( String expr ) { return createKey( new BoxStringLiteral( expr, null, expr ) ); } @@ -194,6 +248,7 @@ public List transformDocumentation( List transformAnnotations( List annotations, Boolean defaultTrue, boolean onlyLiteralValues ) { List> members = new ArrayList<>(); + annotations.forEach( annotation -> { List annotationKey = createKey( annotation.getKey().getValue() ); members.add( annotationKey ); @@ -202,15 +257,14 @@ public List transformAnnotations( List annotati if ( thisValue != null ) { // Literal values are transformed directly if ( thisValue.isLiteral() ) { - value = transform( thisValue, TransformerContext.NONE, ReturnValueContext.EMPTY ); + value = transform( thisValue, TransformerContext.NONE, ReturnValueContext.VALUE ); } // gonna try commenting this out - // else if ( onlyLiteralValues ) { - // // Runtime expressions we just put this place holder text in for - // value = List.of( new LdcInsnNode( "" ) ); - // } - else { - value = transform( thisValue, TransformerContext.NONE, ReturnValueContext.EMPTY ); + else if ( onlyLiteralValues ) { + // Runtime expressions we just put this place holder text in for + value = List.of( new LdcInsnNode( "" ) ); + } else { + value = transform( thisValue, TransformerContext.NONE, ReturnValueContext.VALUE ); } } else if ( defaultTrue ) { // Annotations in tags with no value default to true string (CF compat) @@ -224,6 +278,7 @@ public List transformAnnotations( List annotati } members.add( value ); } ); + if ( annotations.isEmpty() ) { return List.of( new TypeInsnNode( Opcodes.NEW, Type.getInternalName( Struct.class ) ), @@ -250,29 +305,29 @@ public List transformAnnotations( List annotati return transformAnnotations( annotations, false, true ); } - public LabelNode getCurrentBreak( String label ) { - return breaks.get( label == null ? "" : label ); - } + // public LabelNode getCurrentBreak( String label ) { + // return breaks.get( label == null ? "" : label ); + // } - public void setCurrentBreak( String label, LabelNode labelNode ) { - this.breaks.put( label == null ? "" : label, labelNode ); - } + // public void setCurrentBreak( String label, LabelNode labelNode ) { + // this.breaks.put( label == null ? "" : label, labelNode ); + // } - public void removeCurrentBreak( String label ) { - this.breaks.remove( label == null ? "" : label ); - } + // public void removeCurrentBreak( String label ) { + // this.breaks.remove( label == null ? "" : label ); + // } - public LabelNode getCurrentContinue( String label ) { - return continues.get( label == null ? "" : label ); - } + // public LabelNode getCurrentContinue( String label ) { + // return continues.get( label == null ? "" : label ); + // } - public void setCurrentContinue( String label, LabelNode labelNode ) { - this.continues.put( label == null ? "" : label, labelNode ); - } + // public void setCurrentContinue( String label, LabelNode labelNode ) { + // this.continues.put( label == null ? "" : label, labelNode ); + // } - public void removeCurrentContinue( String label ) { - this.continues.remove( label == null ? "" : label ); - } + // public void removeCurrentContinue( String label ) { + // this.continues.remove( label == null ? "" : label ); + // } public void addImport( BoxExpression expression, BoxIdentifier alias ) { imports.add( ImportDefinition.parse( alias == null diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArgumentDeclarationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArgumentDeclarationTransformer.java index 80f231f23..8a7b9fcc8 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArgumentDeclarationTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArgumentDeclarationTransformer.java @@ -21,20 +21,25 @@ import java.util.List; import java.util.Optional; +import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.TypeInsnNode; +import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxExpression; import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.statement.BoxArgumentDeclaration; +import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; import ortus.boxlang.runtime.types.DefaultExpression; @@ -57,7 +62,8 @@ public List transform( BoxNode node, TransformerContext contex if ( boxArgument.getValue().isLiteral() ) { defaultLiteral = transpiler.transform( boxArgument.getValue(), TransformerContext.NONE ); } else { - defaultExpression = transpiler.transform( boxArgument.getValue(), TransformerContext.NONE, ReturnValueContext.VALUE ); + defaultExpression = getDefaultExpression( boxArgument.getValue() ); + // defaultExpression = transpiler.transform( boxArgument.getValue(), TransformerContext.NONE, ReturnValueContext.VALUE ); } } @@ -68,6 +74,8 @@ public List transform( BoxNode node, TransformerContext contex nodes.add( new LdcInsnNode( Optional.ofNullable( boxArgument.getType() ).orElse( "any" ) ) ); nodes.addAll( transpiler.createKey( boxArgument.getName() ) ); nodes.addAll( defaultLiteral ); + nodes.add( new LdcInsnNode( "DEBUG -args" ) ); + nodes.add( new InsnNode( Opcodes.POP ) ); nodes.addAll( defaultExpression ); nodes.addAll( transpiler.transformAnnotations( boxArgument.getAnnotations() ) ); nodes.addAll( transpiler.transformDocumentation( boxArgument.getDocumentation() ) ); @@ -85,4 +93,70 @@ public List transform( BoxNode node, TransformerContext contex false ) ); return nodes; } + + private List getDefaultExpression( BoxExpression body ) { + Type type = Type.getType( "L" + transpiler.getProperty( "packageName" ).replace( '.', '/' ) + + "/" + transpiler.getProperty( "classname" ) + + "$Lambda_" + transpiler.incrementAndGetLambdaCounter() + ";" ); + + ClassNode classNode = new ClassNode(); + + classNode.visit( + Opcodes.V17, + Opcodes.ACC_PUBLIC, + type.getInternalName(), + null, + Type.getInternalName( Object.class ), + new String[] { Type.getInternalName( DefaultExpression.class ) } ); + + MethodVisitor initVisitor = classNode.visitMethod( Opcodes.ACC_PUBLIC, + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + null, + null ); + initVisitor.visitCode(); + initVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + initVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( Object.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + initVisitor.visitInsn( Opcodes.RETURN ); + initVisitor.visitEnd(); + + MethodContextTracker t = new MethodContextTracker( false ); + transpiler.addMethodContextTracker( t ); + // Object evaluate( IBoxContext context ); + MethodVisitor methodVisitor = classNode.visitMethod( + Opcodes.ACC_PUBLIC, + "evaluate", + Type.getMethodDescriptor( Type.getType( Object.class ), Type.getType( IBoxContext.class ) ), + null, + null ); + methodVisitor.visitCode(); + + t.trackNewContext(); + + transpiler.transform( body, TransformerContext.NONE, ReturnValueContext.VALUE_OR_NULL ) + .forEach( ( ins ) -> ins.accept( methodVisitor ) ); + + methodVisitor.visitInsn( Opcodes.ARETURN ); + methodVisitor.visitMaxs( 0, 0 ); + methodVisitor.visitEnd(); + + transpiler.popMethodContextTracker(); + + transpiler.setAuxiliary( type.getClassName(), classNode ); + + List nodes = new ArrayList(); + + nodes.add( new TypeInsnNode( Opcodes.NEW, type.getInternalName() ) ); + nodes.add( new InsnNode( Opcodes.DUP ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKESPECIAL, + type.getInternalName(), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ) ); + return nodes; + } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArrayLiteralTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArrayLiteralTransformer.java index 39c9be122..3a4fd38cf 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArrayLiteralTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxArrayLiteralTransformer.java @@ -44,7 +44,8 @@ public BoxArrayLiteralTransformer( AsmTranspiler transpiler ) { public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { BoxArrayLiteral arrayLiteral = ( BoxArrayLiteral ) node; List nodes = new ArrayList<>(); - nodes.addAll( AsmHelper.array( Type.getType( Object.class ), arrayLiteral.getValues(), ( value, i ) -> transpiler.transform( value, context ) ) ); + nodes.addAll( AsmHelper.array( Type.getType( Object.class ), arrayLiteral.getValues(), + ( value, i ) -> transpiler.transform( value, context, ReturnValueContext.VALUE_OR_NULL ) ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, Type.getInternalName( Array.class ), "of", diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java index 0598d9a07..27b050c71 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java @@ -24,6 +24,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; @@ -86,11 +87,22 @@ public List transform( BoxNode node, TransformerContext contex public List transformEquals( BoxExpression left, List jRight, BoxAssignmentOperator op, List modifiers ) throws IllegalStateException { - boolean hasVar = hasVar( modifiers ); - Optional tracker = transpiler.getCurrentMethodContextTracker(); + boolean hasVar = hasVar( modifiers ); + boolean hasStatic = hasStatic( modifiers ); + boolean hasFinal = hasFinal( modifiers ); + String mustBeScopeName = null; + Optional tracker = transpiler.getCurrentMethodContextTracker(); // "#arguments.scope#.#arguments.propertyName#" = arguments.propertyValue; if ( left instanceof BoxStringInterpolation || left instanceof BoxStringLiteral ) { + if ( hasVar ) { + throw new ExpressionException( "You cannot use the [var] keyword with a quoted string on the left hand side of your assignment", + left.getPosition(), left.getSourceText() ); + } + if ( hasStatic ) { + throw new ExpressionException( "You cannot use the [static] keyword with a quoted string on the left hand side of your assignment", + left.getPosition(), left.getSourceText() ); + } /* * ExpressionInterpreter.setVariable( * ${contextName}, @@ -139,8 +151,13 @@ public List transformEquals( BoxExpression left, List transformEquals( BoxExpression left, List nodes = new ArrayList<>(); if ( furthestLeft instanceof BoxIdentifier id ) { if ( transpiler.matchesImport( id.getName() ) && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { @@ -167,15 +205,32 @@ public List transformEquals( BoxExpression left, List nodes.addAll( t.loadCurrentContext() ) ); + tracker.ifPresent( t -> { + nodes.addAll( t.loadCurrentContext() ); + } ); + + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, Type.getInternalName( Boolean.class ), hasFinal ? "TRUE" : "FALSE", + Type.getDescriptor( Boolean.class ) ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( Boolean.class ), + "booleanValue", + Type.getMethodDescriptor( Type.getType( boolean.class ) ), + false ) ); + + if ( mustBeScopeName != null ) { + nodes.addAll( transpiler.createKey( mustBeScopeName ) ); + } else { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } tracker.ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); - List keyNode = transpiler.createKey( id.getName() ); - nodes.addAll( keyNode ); + nodes.addAll( transpiler.createKey( id.getName() ) ); tracker.ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, Type.getInternalName( IBoxContext.class ), @@ -198,6 +253,8 @@ public List transformEquals( BoxExpression left, List transformEquals( BoxExpression left, List nodes.addAll( t.loadCurrentContext() ) ); + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, Type.getInternalName( Boolean.class ), hasFinal ? "TRUE" : "FALSE", + Type.getDescriptor( Boolean.class ) ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( Boolean.class ), + "booleanValue", + Type.getMethodDescriptor( Type.getType( boolean.class ) ), + false ) ); + + if ( mustBeScopeName != null ) { + nodes.addAll( transpiler.createKey( mustBeScopeName ) ); + } else { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } + nodes.addAll( transpiler.transform( furthestLeft, TransformerContext.NONE, ReturnValueContext.VALUE ) ); nodes.addAll( jRight ); @@ -228,6 +301,8 @@ public List transformEquals( BoxExpression left, List modifiers ) { return modifiers.stream().anyMatch( it -> it == BoxAssignmentModifier.VAR ); } + private boolean hasStatic( List modifiers ) { + return modifiers.stream().anyMatch( it -> it == BoxAssignmentModifier.STATIC ); + } + + private boolean hasFinal( List modifiers ) { + return modifiers.stream().anyMatch( it -> it == BoxAssignmentModifier.FINAL ); + } + private Class getMethodCallTemplate( BoxAssignment assignment ) { BoxAssignmentOperator operator = assignment.getOp(); return switch ( operator ) { diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxBreakTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxBreakTransformer.java index 8950fd26d..a4749603a 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxBreakTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxBreakTransformer.java @@ -26,12 +26,22 @@ import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; +import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.expression.BoxClosure; +import ortus.boxlang.compiler.ast.expression.BoxLambda; import ortus.boxlang.compiler.ast.statement.BoxBreak; +import ortus.boxlang.compiler.ast.statement.BoxDo; +import ortus.boxlang.compiler.ast.statement.BoxForIn; +import ortus.boxlang.compiler.ast.statement.BoxForIndex; +import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxSwitch; +import ortus.boxlang.compiler.ast.statement.BoxWhile; +import ortus.boxlang.compiler.ast.statement.component.BoxComponent; import ortus.boxlang.runtime.components.Component; public class BoxBreakTransformer extends AbstractTransformer { @@ -45,14 +55,20 @@ public List transform( BoxNode node, TransformerContext contex BoxBreak breakNode = ( BoxBreak ) node; ExitsAllowed exitsAllowed = getExitsAllowed( node ); - LabelNode currentBreak = transpiler.getCurrentBreak( breakNode.getLabel() ); + MethodContextTracker tracker = transpiler.getCurrentMethodContextTracker().get(); List nodes = new ArrayList(); if ( returnContext.nullable || exitsAllowed.equals( ExitsAllowed.FUNCTION ) ) { nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); } + BoxNode labelTarget = tracker.getStringLabel( breakNode.getLabel() ); + LabelNode currentBreak = tracker.getBreak( labelTarget != null ? labelTarget : getTargetAncestor( breakNode ) ); + if ( currentBreak != null ) { + if ( returnContext.nullable && nodes.size() == 0 ) { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } nodes.add( new JumpInsnNode( Opcodes.GOTO, currentBreak ) ); return nodes; } @@ -84,4 +100,10 @@ public List transform( BoxNode node, TransformerContext contex throw new RuntimeException( "Cannot break from current location" ); } + + public BoxNode getTargetAncestor( BoxNode node ) { + return node.getFirstNodeOfTypes( BoxSwitch.class, BoxFunctionDeclaration.class, BoxClosure.class, BoxLambda.class, BoxComponent.class, BoxDo.class, + BoxForIndex.class, BoxForIn.class, + BoxWhile.class ); + } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxClosureTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxClosureTransformer.java index aee83d060..7ad7596ac 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxClosureTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxClosureTransformer.java @@ -33,6 +33,7 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.expression.BoxClosure; +import ortus.boxlang.compiler.ast.statement.BoxStatementBlock; import ortus.boxlang.compiler.parser.BoxSourceType; import ortus.boxlang.runtime.context.FunctionBoxContext; import ortus.boxlang.runtime.context.IBoxContext; @@ -139,10 +140,15 @@ public List transform( BoxNode node, TransformerContext contex "getSourceType", Type.getType( BoxSourceType.class ) ); + boolean isBlock = boxClosure.getBody() instanceof BoxStatementBlock; + + int componentCounter = transpiler.getComponentCounter(); + transpiler.setComponentCounter( 0 ); AsmHelper.methodWithContextAndClassLocator( classNode, "_invoke", Type.getType( FunctionBoxContext.class ), Type.getType( Object.class ), false, - transpiler, false, + transpiler, isBlock, () -> boxClosure.getBody().getChildren().stream().flatMap( statement -> transpiler.transform( statement, TransformerContext.NONE ).stream() ) .toList() ); + transpiler.setComponentCounter( componentCounter ); AsmHelper.complete( classNode, type, methodVisitor -> { methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxComparisonOperationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxComparisonOperationTransformer.java index 5bcaf8093..a5e13c947 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxComparisonOperationTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxComparisonOperationTransformer.java @@ -17,10 +17,14 @@ */ package ortus.boxlang.compiler.asmboxpiler.transformer.expression; +import java.util.ArrayList; +import java.util.List; + import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodInsnNode; + import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; @@ -28,12 +32,16 @@ import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.expression.BoxComparisonOperation; import ortus.boxlang.compiler.ast.expression.BoxComparisonOperator; -import ortus.boxlang.runtime.operators.*; +import ortus.boxlang.runtime.operators.EqualsEquals; +import ortus.boxlang.runtime.operators.EqualsEqualsEquals; +import ortus.boxlang.runtime.operators.GreaterThan; +import ortus.boxlang.runtime.operators.GreaterThanEqual; +import ortus.boxlang.runtime.operators.LessThan; +import ortus.boxlang.runtime.operators.LessThanEqual; +import ortus.boxlang.runtime.operators.Not; +import ortus.boxlang.runtime.operators.NotEqualsEquals; import ortus.boxlang.runtime.types.exceptions.ExpressionException; -import java.util.ArrayList; -import java.util.List; - public class BoxComparisonOperationTransformer extends AbstractTransformer { public BoxComparisonOperationTransformer( Transpiler transpiler ) { @@ -45,6 +53,7 @@ public List transform( BoxNode node, TransformerContext contex BoxComparisonOperation operation = ( BoxComparisonOperation ) node; List left = transpiler.transform( operation.getLeft(), TransformerContext.NONE, ReturnValueContext.VALUE ); List right = transpiler.transform( operation.getRight(), TransformerContext.NONE, ReturnValueContext.VALUE ); + boolean negated = false; Class dispatcher; if ( operation.getOperator() == BoxComparisonOperator.Equal ) { @@ -53,6 +62,9 @@ public List transform( BoxNode node, TransformerContext contex dispatcher = NotEqualsEquals.class; } else if ( operation.getOperator() == BoxComparisonOperator.TEqual ) { dispatcher = EqualsEqualsEquals.class; + } else if ( operation.getOperator() == BoxComparisonOperator.TNotEqual ) { + dispatcher = EqualsEqualsEquals.class; + negated = true; } else if ( operation.getOperator() == BoxComparisonOperator.GreaterThan ) { dispatcher = GreaterThan.class; } else if ( operation.getOperator() == BoxComparisonOperator.GreaterThanEquals ) { @@ -70,6 +82,18 @@ public List transform( BoxNode node, TransformerContext contex nodes.addAll( right ); nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, Type.getInternalName( dispatcher ), "invoke", Type.getMethodDescriptor( Type.getType( Boolean.class ), Type.getType( Object.class ), Type.getType( Object.class ) ), false ) ); + + if ( negated ) { + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Not.class ), + "invoke", + Type.getMethodDescriptor( Type.getType( Boolean.class ), Type.getType( Object.class ) ), + false + ) + ); + } return nodes; } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxContinueTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxContinueTransformer.java index 5a943affc..98e966885 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxContinueTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxContinueTransformer.java @@ -23,12 +23,22 @@ import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.LabelNode; +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; +import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.expression.BoxClosure; +import ortus.boxlang.compiler.ast.expression.BoxLambda; import ortus.boxlang.compiler.ast.statement.BoxContinue; +import ortus.boxlang.compiler.ast.statement.BoxDo; +import ortus.boxlang.compiler.ast.statement.BoxForIn; +import ortus.boxlang.compiler.ast.statement.BoxForIndex; +import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxWhile; +import ortus.boxlang.compiler.ast.statement.component.BoxComponent; public class BoxContinueTransformer extends AbstractTransformer { @@ -41,14 +51,21 @@ public List transform( BoxNode node, TransformerContext contex BoxContinue continueNode = ( BoxContinue ) node; ExitsAllowed exitsAllowed = getExitsAllowed( node ); - LabelNode currentBreak = transpiler.getCurrentContinue( continueNode.getLabel() ); List nodes = new ArrayList(); + AsmHelper.addDebugLabel( nodes, "BoxContinue" ); if ( returnContext.nullable || exitsAllowed.equals( ExitsAllowed.FUNCTION ) ) { nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); } + MethodContextTracker tracker = transpiler.getCurrentMethodContextTracker().get(); + BoxNode labelTarget = tracker.getStringLabel( continueNode.getLabel() ); + LabelNode currentBreak = tracker.getContinue( labelTarget != null ? labelTarget : getTargetAncestor( continueNode ) ); + if ( currentBreak != null ) { + if ( returnContext.nullable && nodes.size() == 0 ) { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } nodes.add( new JumpInsnNode( Opcodes.GOTO, currentBreak ) ); return nodes; } @@ -56,6 +73,8 @@ public List transform( BoxNode node, TransformerContext contex if ( exitsAllowed.equals( ExitsAllowed.COMPONENT ) ) { // template = "if(true) return Component.BodyResult.ofBreak(" + componentLabel + ");"; } else if ( exitsAllowed.equals( ExitsAllowed.LOOP ) ) { + nodes.add( new JumpInsnNode( Opcodes.GOTO, currentBreak ) ); + return nodes; // template = "if(true) break " + breakLabel + ";"; } else if ( exitsAllowed.equals( ExitsAllowed.FUNCTION ) ) { nodes.add( new InsnNode( Opcodes.ARETURN ) ); @@ -67,7 +86,18 @@ public List transform( BoxNode node, TransformerContext contex // throw new RuntimeException( "Cannot break from current location" ); // } - throw new RuntimeException( "Cannot break from current location" ); + throw new RuntimeException( "Cannot continue from current location" ); + + } + + @SuppressWarnings( "unchecked" ) + public BoxNode getTargetAncestor( BoxNode node ) { + return node.getFirstNodeOfTypes( BoxFunctionDeclaration.class, BoxClosure.class, BoxLambda.class, BoxComponent.class, BoxDo.class, + BoxForIndex.class, BoxForIn.class, + BoxWhile.class ); + } + private boolean isLoop( BoxNode node ) { + return node instanceof BoxForIndex; } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxExpressionInvocationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxExpressionInvocationTransformer.java new file mode 100644 index 000000000..d27482bc5 --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxExpressionInvocationTransformer.java @@ -0,0 +1,63 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.expression; + +import java.util.ArrayList; +import java.util.List; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; +import ortus.boxlang.compiler.asmboxpiler.AsmTranspiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.expression.BoxExpressionInvocation; +import ortus.boxlang.compiler.ast.expression.BoxLambda; +import ortus.boxlang.runtime.scopes.Key; + +public class BoxExpressionInvocationTransformer extends AbstractTransformer { + + public BoxExpressionInvocationTransformer( AsmTranspiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { + BoxExpressionInvocation invocation = ( BoxExpressionInvocation ) node; + + List nameNodes = transpiler.transform( invocation.getExpr(), context, ReturnValueContext.VALUE ); + + List nodes = new ArrayList<>(); + nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); + + Type invokeType = invocation.getExpr() instanceof BoxLambda || invocation.getExpr().getDescendantsOfType( BoxLambda.class ).size() > 0 + ? Type.getType( Object.class ) + : Type.getType( Key.class ); + + nodes.addAll( AsmHelper.callinvokeFunction( transpiler, invokeType, invocation.getArguments(), nameNodes, context, false ) ); + + if ( returnContext.empty ) { + nodes.add( new InsnNode( Opcodes.POP ) ); + } + + return nodes; + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxFunctionInvocationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxFunctionInvocationTransformer.java index 802012fbd..dd6a6c202 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxFunctionInvocationTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxFunctionInvocationTransformer.java @@ -21,7 +21,6 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnNode; -import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.VarInsnNode; import ortus.boxlang.compiler.asmboxpiler.AsmHelper; @@ -31,7 +30,6 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.expression.BoxFunctionInvocation; -import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.scopes.Key; public class BoxFunctionInvocationTransformer extends AbstractTransformer { @@ -43,20 +41,24 @@ public BoxFunctionInvocationTransformer( AsmTranspiler transpiler ) { @Override public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { BoxFunctionInvocation function = ( BoxFunctionInvocation ) node; - TransformerContext safe = function.getName().equalsIgnoreCase( "isnull" ) ? TransformerContext.SAFE : context; + boolean safe = function.getName().equalsIgnoreCase( "isnull" ) ? true : false; List nodes = new ArrayList<>(); nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); - nodes.addAll( transpiler.createKey( function.getName() ) ); + // nodes.addAll( transpiler.createKey( function.getName() ) ); - nodes.addAll( AsmHelper.array( Type.getType( Object.class ), function.getArguments(), - ( argument, i ) -> transpiler.transform( argument, safe, ReturnValueContext.VALUE ) ) ); + TransformerContext argContext = safe ? TransformerContext.SAFE : context; + nodes.addAll( AsmHelper.callinvokeFunction( transpiler, Type.getType( Key.class ), function.getArguments(), transpiler.createKey( function.getName() ), + argContext, safe ) ); - nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, - Type.getInternalName( IBoxContext.class ), - "invokeFunction", - Type.getMethodDescriptor( Type.getType( Object.class ), Type.getType( Key.class ), Type.getType( Object[].class ) ), - true ) ); + // nodes.addAll( AsmHelper.array( Type.getType( Object.class ), function.getArguments(), + // ( argument, i ) -> transpiler.transform( argument, safe, ReturnValueContext.VALUE ) ) ); + + // nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, + // Type.getInternalName( IBoxContext.class ), + // "invokeFunction", + // Type.getMethodDescriptor( Type.getType( Object.class ), Type.getType( Key.class ), Type.getType( Object[].class ) ), + // true ) ); if ( returnContext.empty ) { nodes.add( new InsnNode( Opcodes.POP ) ); diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxLambdaTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxLambdaTransformer.java index 9e6690714..8b917ed8a 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxLambdaTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxLambdaTransformer.java @@ -20,6 +20,7 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; import ortus.boxlang.compiler.asmboxpiler.AsmHelper; @@ -118,8 +119,14 @@ public List transform( BoxNode node, TransformerContext contex AsmHelper.methodWithContextAndClassLocator( classNode, "_invoke", Type.getType( FunctionBoxContext.class ), Type.getType( Object.class ), false, transpiler, false, - () -> boxLambda.getBody().getChildren().stream().flatMap( statement -> transpiler.transform( statement, TransformerContext.NONE ).stream() ) - .toList() ); + () -> { + if ( boxLambda.getBody().getChildren().size() == 0 ) { + return List.of( new InsnNode( Opcodes.ACONST_NULL ) ); + } + return boxLambda.getBody().getChildren().stream() + .flatMap( statement -> transpiler.transform( statement, TransformerContext.NONE, ReturnValueContext.VALUE_OR_NULL ).stream() ) + .toList(); + } ); AsmHelper.complete( classNode, type, methodVisitor -> { methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxMethodInvocationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxMethodInvocationTransformer.java index 610e071a1..2b32d1771 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxMethodInvocationTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxMethodInvocationTransformer.java @@ -21,11 +21,8 @@ import java.util.List; import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnNode; -import org.objectweb.asm.tree.MethodInsnNode; import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.Transpiler; @@ -35,9 +32,6 @@ import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.expression.BoxIdentifier; import ortus.boxlang.compiler.ast.expression.BoxMethodInvocation; -import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.dynamic.Referencer; -import ortus.boxlang.runtime.scopes.Key; public class BoxMethodInvocationTransformer extends AbstractTransformer { @@ -56,31 +50,14 @@ public List transform( BoxNode node, TransformerContext contex nodes.addAll( transpiler.transform( invocation.getObj(), context, ReturnValueContext.VALUE ) ); + List name = null; if ( invocation.getUsedDotAccess() ) { - nodes.addAll( transpiler.createKey( ( ( BoxIdentifier ) invocation.getName() ).getName() ) ); + name = transpiler.createKey( ( ( BoxIdentifier ) invocation.getName() ).getName() ); } else { - nodes.addAll( transpiler.createKey( invocation.getName() ) ); + name = transpiler.createKey( invocation.getName() ); } - nodes - .addAll( AsmHelper.array( Type.getType( Object.class ), invocation.getArguments(), - ( argument, i ) -> transpiler.transform( argument, context, ReturnValueContext.VALUE ) ) ); - - nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, - Type.getInternalName( Boolean.class ), - safe.toString().toUpperCase(), - Type.getDescriptor( Boolean.class ) ) ); - - nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, - Type.getInternalName( Referencer.class ), - "getAndInvoke", - Type.getMethodDescriptor( Type.getType( Object.class ), - Type.getType( IBoxContext.class ), - Type.getType( Object.class ), - Type.getType( Key.class ), - Type.getType( Object[].class ), - Type.getType( Boolean.class ) ), - false ) ); + nodes.addAll( AsmHelper.callReferencerGetAndInvoke( transpiler, invocation.getArguments(), name, context, safe ) ); if ( returnContext.empty ) { nodes.add( new InsnNode( Opcodes.POP ) ); diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxNewTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxNewTransformer.java index 48df92baa..57b5b9afa 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxNewTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxNewTransformer.java @@ -55,7 +55,7 @@ public List transform( BoxNode node, TransformerContext contex nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); nodes.add( new LdcInsnNode( "" ) ); // TODO: how to set this? - nodes.addAll( transpiler.transform( boxNew.getExpression(), TransformerContext.NONE ) ); + nodes.addAll( transpiler.transform( boxNew.getExpression(), TransformerContext.NONE, ReturnValueContext.VALUE ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, Type.getInternalName( StringCaster.class ), "cast", @@ -83,17 +83,8 @@ public List transform( BoxNode node, TransformerContext contex false ) ); nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); - nodes.addAll( - AsmHelper.array( Type.getType( Object.class ), boxNew.getArguments(), - ( argument, i ) -> transpiler.transform( boxNew.getArguments().get( i ), context ) ) ); - nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, - Type.getInternalName( DynamicObject.class ), - "invokeConstructor", - Type.getMethodDescriptor( Type.getType( DynamicObject.class ), - Type.getType( IBoxContext.class ), - Type.getType( Object[].class ) ), - false ) ); + nodes.addAll( AsmHelper.callDynamicObjectInvokeConstructor( transpiler, boxNew.getArguments(), context ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, Type.getInternalName( DynamicObject.class ), diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxReturnTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxReturnTransformer.java index b676f508d..616dd6b23 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxReturnTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxReturnTransformer.java @@ -19,8 +19,10 @@ import java.util.List; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodInsnNode; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; @@ -28,6 +30,7 @@ import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.statement.BoxReturn; +import ortus.boxlang.runtime.components.Component; public class BoxReturnTransformer extends AbstractTransformer { @@ -42,6 +45,18 @@ public List transform( BoxNode node, TransformerContext contex List nodes = new ArrayList<>(); if ( boxReturn.getExpression() == null ) { nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } else if ( transpiler.isInsideComponent() ) { + nodes.addAll( transpiler.transform( boxReturn.getExpression(), TransformerContext.NONE, ReturnValueContext.VALUE_OR_NULL ) ); + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Component.BodyResult.class ), + "ofReturn", + Type.getMethodDescriptor( Type.getType( Component.BodyResult.class ), Type.getType( Object.class ) ), + false + ) + ); + // template = "return Component.BodyResult.ofReturn( ${expr} );"; } else { nodes.addAll( transpiler.transform( boxReturn.getExpression(), TransformerContext.NONE, ReturnValueContext.VALUE_OR_NULL ) ); } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStatementBlockTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStatementBlockTransformer.java index 457d79b17..74ef035b3 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStatementBlockTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStatementBlockTransformer.java @@ -14,6 +14,7 @@ */ package ortus.boxlang.compiler.asmboxpiler.transformer.expression; +import java.util.ArrayList; import java.util.List; import org.objectweb.asm.tree.AbstractInsnNode; @@ -36,13 +37,17 @@ public BoxStatementBlockTransformer( Transpiler transpiler ) { @Override public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { - BoxStatementBlock boxStatementBlock = ( BoxStatementBlock ) node; + BoxStatementBlock boxStatementBlock = ( BoxStatementBlock ) node; + List nodes = new ArrayList(); + + AsmHelper.addDebugLabel( nodes, "BoxStatementBlock" ); if ( boxStatementBlock.getBody().size() == 0 && ReturnValueContext.VALUE_OR_NULL == returnContext ) { - return List.of( new InsnNode( Opcode.ACONST_NULL ) ); + nodes.add( new InsnNode( Opcode.ACONST_NULL ) ); + return nodes; } - List nodes = AsmHelper.transformBodyExpressions( transpiler, boxStatementBlock.getBody(), context, returnContext ); + nodes.addAll( AsmHelper.transformBodyExpressions( transpiler, boxStatementBlock.getBody(), context, returnContext ) ); return nodes; } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStaticAccessTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStaticAccessTransformer.java new file mode 100644 index 000000000..a8bdc9a6e --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStaticAccessTransformer.java @@ -0,0 +1,136 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +package ortus.boxlang.compiler.asmboxpiler.transformer.expression; + +import java.util.ArrayList; +import java.util.List; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.expression.BoxFQN; +import ortus.boxlang.compiler.ast.expression.BoxIdentifier; +import ortus.boxlang.compiler.ast.expression.BoxIntegerLiteral; +import ortus.boxlang.compiler.ast.expression.BoxStaticAccess; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.dynamic.Referencer; +import ortus.boxlang.runtime.interop.DynamicObject; +import ortus.boxlang.runtime.runnables.BoxClassSupport; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.exceptions.ExpressionException; + +public class BoxStaticAccessTransformer extends AbstractTransformer { + + public BoxStaticAccessTransformer( Transpiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { + BoxStaticAccess objectAccess = ( BoxStaticAccess ) node; + List nodes = new ArrayList<>(); + Boolean safe = objectAccess.isSafe() || context == TransformerContext.SAFE; + + transpiler.getCurrentMethodContextTracker().ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); + + // does this go here or above? + + // get the imports for the current class + transpiler.getCurrentMethodContextTracker().ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); + // nodes.add( new VarInsnNode( Opcodes.ALOAD, 0 ) ); + // push scope reference onto the stack + if ( objectAccess.getContext() instanceof BoxFQN fqn ) { + nodes.add( new LdcInsnNode( fqn.getValue() ) ); + } else if ( objectAccess.getContext() instanceof BoxIdentifier id ) { + if ( transpiler.matchesImport( id.getName() ) && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { + nodes.addAll( transpiler.transform( id, context, ReturnValueContext.VALUE ) ); + } else { + nodes.add( new LdcInsnNode( id.getName() ) ); + } + } else { + throw new ExpressionException( "Unexpected base token in static access.", objectAccess.getContext() ); + } + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, transpiler.getProperty( "classTypeInternal" ), "imports", Type.getDescriptor( List.class ) ) ); + + // invoke BoxClassSupport.ensureClass(${contextName},${scopeReference},imports), + nodes.add( new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( BoxClassSupport.class ), + "ensureClass", + Type.getMethodDescriptor( Type.getType( DynamicObject.class ), + Type.getType( IBoxContext.class ), + Type.getType( Object.class ), + Type.getType( List.class ) + ), + false ) + ); + + // Node accessKey; + // objectAccess just uses the string directly, array access allows any expression + if ( objectAccess.getAccess() instanceof BoxIdentifier id ) { + nodes.addAll( transpiler.createKey( id.getName() ) ); + } else if ( objectAccess.getAccess() instanceof BoxIntegerLiteral il ) { + nodes.addAll( transpiler.createKey( il ) ); + } else { + throw new ExpressionException( "Unsupported access type: " + objectAccess.getAccess().getClass().getName(), objectAccess.getAccess() ); + } + + // Expression jContext; + + // // "scope" here isn't a BoxLang proper scope, it's just whatever Java source represents the context of the access expression + // values.put( "scopeReference", jContext.toString() ); + + // String template = """ + // Referencer.get( + // ${contextName}, + // BoxClassSupport.ensureClass(${contextName},${scopeReference},imports), + // ${accessKey}, + // ${safe} + // ) + // """; + + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, Type.getInternalName( Boolean.class ), safe ? "TRUE" : "FALSE", + Type.getDescriptor( Boolean.class ) ) ); + + nodes.add( new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Referencer.class ), + "get", + Type.getMethodDescriptor( Type.getType( Object.class ), + Type.getType( IBoxContext.class ), + Type.getType( Object.class ), + Type.getType( Key.class ), + Type.getType( Boolean.class ) + ), + false ) + ); + + // Node javaExpr = parseExpression( template, values ); + // logger.trace( node.getSourceText() + " -> " + javaExpr ); + // addIndex( javaExpr, node ); + return nodes; + } + +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStaticMethodInvocationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStaticMethodInvocationTransformer.java new file mode 100644 index 000000000..6d40a3dd2 --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStaticMethodInvocationTransformer.java @@ -0,0 +1,91 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.expression; + +import java.util.ArrayList; +import java.util.List; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxExpression; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.expression.BoxFQN; +import ortus.boxlang.compiler.ast.expression.BoxIdentifier; +import ortus.boxlang.compiler.ast.expression.BoxStaticMethodInvocation; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.interop.DynamicObject; +import ortus.boxlang.runtime.runnables.BoxClassSupport; +import ortus.boxlang.runtime.types.exceptions.ExpressionException; + +public class BoxStaticMethodInvocationTransformer extends AbstractTransformer { + + public BoxStaticMethodInvocationTransformer( Transpiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnValueContext ) throws IllegalStateException { + BoxStaticMethodInvocation invocation = ( BoxStaticMethodInvocation ) node; + List nodes = new ArrayList<>(); + BoxExpression baseObject = invocation.getObj(); + // Expression expr; + + transpiler.getCurrentMethodContextTracker().ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); + + nodes.add( new InsnNode( Opcodes.DUP ) ); + + if ( baseObject instanceof BoxFQN fqn ) { + nodes.add( new LdcInsnNode( fqn.getValue() ) ); + } else if ( baseObject instanceof BoxIdentifier id ) { + nodes.addAll( transpiler.transform( id, context, ReturnValueContext.VALUE ) ); + } else { + throw new ExpressionException( "Unexpected base token in static method access.", baseObject ); + } + + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, transpiler.getProperty( "classTypeInternal" ), "imports", + Type.getDescriptor( List.class ) ) ); + + // invoke BoxClassSupport.ensureClass(${contextName},${scopeReference},imports), + nodes.add( new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( BoxClassSupport.class ), + "ensureClass", + Type.getMethodDescriptor( Type.getType( DynamicObject.class ), + Type.getType( IBoxContext.class ), + Type.getType( Object.class ), + Type.getType( List.class ) + ), + false ) + ); + + nodes.addAll( AsmHelper.callReferencerGetAndInvoke( transpiler, invocation.getArguments(), invocation.getName().toString(), context, false ) ); + + return nodes; + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringConcatTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringConcatTransformer.java index 0d51ea2e4..2a2fcde25 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringConcatTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringConcatTransformer.java @@ -44,7 +44,8 @@ public BoxStringConcatTransformer( Transpiler transpiler ) { public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { BoxStringConcat interpolation = ( BoxStringConcat ) node; if ( interpolation.getValues().size() == 1 ) { - return transpiler.transform( interpolation.getValues().get( 0 ), TransformerContext.NONE ); + // TODO should this invoke StringCaster? + return transpiler.transform( interpolation.getValues().get( 0 ), TransformerContext.NONE, returnContext ); } else { List nodes = new ArrayList<>(); nodes.addAll( AsmHelper.array( Type.getType( Object.class ), interpolation.getValues(), diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringLiteralTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringLiteralTransformer.java index 7dcf040a0..3cae1e7df 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringLiteralTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxStringLiteralTransformer.java @@ -14,11 +14,16 @@ */ package ortus.boxlang.compiler.asmboxpiler.transformer.expression; +import java.util.ArrayList; import java.util.List; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; @@ -28,13 +33,64 @@ public class BoxStringLiteralTransformer extends AbstractTransformer { + private static final int MAX_LITERAL_LENGTH = 30000; // 64KB limit + public BoxStringLiteralTransformer( Transpiler transpiler ) { super( transpiler ); } @Override public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { - BoxStringLiteral literal = ( BoxStringLiteral ) node; - return List.of( new LdcInsnNode( literal.getValue() ) ); + BoxStringLiteral literal = ( BoxStringLiteral ) node; + + String value = literal.getValue(); + + if ( value.length() < MAX_LITERAL_LENGTH ) { + return List.of( new LdcInsnNode( literal.getValue() ) ); + + } + + List nodes = new ArrayList(); + List parts = splitStringIntoParts( value ); + + nodes.add( new LdcInsnNode( "" ) ); + nodes.addAll( + AsmHelper.array( Type.getType( String.class ), parts.stream().map( s -> { + List x = List.of( new LdcInsnNode( s ) ); + + return x; + } + ).toList() ) ); + + nodes.add( new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( String.class ), + "join", + Type.getMethodDescriptor( Type.getType( String.class ), + Type.getType( CharSequence.class ), + Type.getType( CharSequence[].class ) + ), + false ) + ); + + return nodes; + } + + /** + * Split a large string into parts + * + * @param str The input string. + * + * @return A list of StringLiteralExpr parts. + **/ + private List splitStringIntoParts( String str ) { + List parts = new ArrayList<>(); + int length = str.length(); + for ( int i = 0; i < length; i += MAX_LITERAL_LENGTH ) { + int end = Math.min( length, i + MAX_LITERAL_LENGTH ); + String part = str.substring( i, end ); + parts.add( part ); + } + return parts; } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxSwitchTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxSwitchTransformer.java index ff7957cbb..cfd415e3d 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxSwitchTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxSwitchTransformer.java @@ -26,6 +26,7 @@ import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; @@ -49,15 +50,21 @@ public List transform( BoxNode node, TransformerContext contex List condition = transpiler.transform( boxSwitch.getCondition(), TransformerContext.NONE, ReturnValueContext.VALUE ); List nodes = new ArrayList<>(); + AsmHelper.addDebugLabel( nodes, "BoxSwitch" ); nodes.addAll( condition ); nodes.add( new LdcInsnNode( 0 ) ); - LabelNode endLabel = new LabelNode(); + LabelNode endLabel = new LabelNode(); + LabelNode breakTarget = new LabelNode(); + + transpiler.getCurrentMethodContextTracker().ifPresent( t -> t.setBreak( boxSwitch, endLabel ) ); + boxSwitch.getCases().forEach( c -> { if ( c.getCondition() == null ) { return; } + AsmHelper.addDebugLabel( nodes, "BoxSwitch - case start" ); LabelNode startOfCase = new LabelNode(), endOfCase = new LabelNode(), endOfAll = new LabelNode(); nodes.add( new JumpInsnNode( Opcodes.IFNE, startOfCase ) ); // this dupes the condition @@ -96,14 +103,15 @@ public List transform( BoxNode node, TransformerContext contex false ) ); nodes.add( new JumpInsnNode( Opcodes.IFEQ, endOfCase ) ); nodes.add( startOfCase ); - transpiler.setCurrentBreak( null, endLabel ); + c.getBody().forEach( stmt -> nodes.addAll( transpiler.transform( stmt, TransformerContext.NONE ) ) ); - transpiler.removeCurrentBreak( null ); // TODO: label name? + nodes.add( new LdcInsnNode( 1 ) ); nodes.add( new JumpInsnNode( Opcodes.GOTO, endOfAll ) ); nodes.add( endOfCase ); nodes.add( new LdcInsnNode( 0 ) ); nodes.add( endOfAll ); + AsmHelper.addDebugLabel( nodes, "BoxSwitch - case end" ); } ); // TODO: Can there be more than one default case? @@ -113,13 +121,33 @@ public List transform( BoxNode node, TransformerContext contex if ( hasDefault ) { throw new ExpressionException( "Multiple default cases not supported", c.getPosition(), c.getSourceText() ); } + AsmHelper.addDebugLabel( nodes, "BoxSwitch - default case" ); hasDefault = true; + + // pop the initial 0 constant in case we didn't match any cases + nodes.add( new InsnNode( Opcodes.POP ) ); + // pop the condition off the stack + // nodes.add( new InsnNode( Opcodes.POP ) ); + c.getBody().forEach( stmt -> nodes.addAll( transpiler.transform( stmt, TransformerContext.NONE ) ) ); + + nodes.add( new JumpInsnNode( Opcodes.GOTO, endLabel ) ); } } + + // pop the initial 0 constant in case we didn't match any cases nodes.add( new InsnNode( Opcodes.POP ) ); nodes.add( endLabel ); + + // pop the condition off the stack nodes.add( new InsnNode( Opcodes.POP ) ); + // nodes.add( breakTarget ); + + if ( !returnContext.empty ) { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } + + AsmHelper.addDebugLabel( nodes, "BoxSwitch - done" ); return nodes; } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxAssertTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxAssertTransformer.java index a249d4e63..31b306b54 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxAssertTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxAssertTransformer.java @@ -20,6 +20,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.VarInsnNode; @@ -43,12 +44,17 @@ public List transform( BoxNode node, TransformerContext contex BoxAssert boxAssert = ( BoxAssert ) node; List nodes = new ArrayList<>(); nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); - nodes.addAll( transpiler.transform( boxAssert.getExpression(), TransformerContext.RIGHT, ReturnValueContext.EMPTY ) ); + nodes.addAll( transpiler.transform( boxAssert.getExpression(), TransformerContext.RIGHT, ReturnValueContext.VALUE ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, Type.getInternalName( Assert.class ), "invoke", Type.getMethodDescriptor( Type.getType( Boolean.class ), Type.getType( IBoxContext.class ), Type.getType( Object.class ) ), false ) ); + + if ( returnContext.empty ) { + nodes.add( new InsnNode( Opcodes.POP ) ); + } + return nodes; } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxBufferOutputTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxBufferOutputTransformer.java index d051a7c9e..01d656087 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxBufferOutputTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxBufferOutputTransformer.java @@ -47,7 +47,8 @@ public List transform( BoxNode node, TransformerContext contex List nodes = new ArrayList<>(); nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); - nodes.addAll( transpiler.transform( bufferOuput.getExpression(), TransformerContext.NONE, ReturnValueContext.EMPTY ) ); + nodes + .addAll( transpiler.transform( bufferOuput.getExpression(), TransformerContext.NONE, ReturnValueContext.VALUE_OR_NULL ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, Type.getInternalName( IBoxContext.class ), "writeToBuffer", diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxClassTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxClassTransformer.java new file mode 100644 index 000000000..15580be95 --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxClassTransformer.java @@ -0,0 +1,896 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.statement; + +import java.io.Serializable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxClass; +import ortus.boxlang.compiler.ast.BoxExpression; +import ortus.boxlang.compiler.ast.Source; +import ortus.boxlang.compiler.ast.SourceFile; +import ortus.boxlang.compiler.ast.expression.BoxStringLiteral; +import ortus.boxlang.compiler.ast.statement.BoxAnnotation; +import ortus.boxlang.compiler.ast.statement.BoxArgumentDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxImport; +import ortus.boxlang.compiler.ast.statement.BoxMethodDeclarationModifier; +import ortus.boxlang.compiler.ast.statement.BoxReturnType; +import ortus.boxlang.compiler.ast.statement.BoxType; +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.dynamic.IReferenceable; +import ortus.boxlang.runtime.dynamic.javaproxy.InterfaceProxyService; +import ortus.boxlang.runtime.loader.ImportDefinition; +import ortus.boxlang.runtime.runnables.BoxClassSupport; +import ortus.boxlang.runtime.runnables.BoxInterface; +import ortus.boxlang.runtime.runnables.IClassRunnable; +import ortus.boxlang.runtime.scopes.ClassVariablesScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.StaticScope; +import ortus.boxlang.runtime.scopes.ThisScope; +import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.Array; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.IType; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.types.meta.BoxMeta; +import ortus.boxlang.runtime.types.util.BLCollector; +import ortus.boxlang.runtime.types.util.ListUtil; +import ortus.boxlang.runtime.util.ResolvedFilePath; + +public class BoxClassTransformer { + + public static final Type CLASS_TYPE = Type.getType( Class.class ); + public static final Type CLASS_ARRAY_TYPE = Type.getType( Class[].class ); + + private static final String EXTENDS_ANNOTATION_MARKER = "overrideJava"; + + public static ClassNode transpile( Transpiler transpiler, BoxClass boxClass ) throws BoxRuntimeException { + Source source = boxClass.getPosition().getSource(); + String sourceType = transpiler.getProperty( "sourceType" ); + + String filePath = source instanceof SourceFile file && file.getFile() != null ? file.getFile().getAbsolutePath() + : "unknown"; + String boxClassName = transpiler.getProperty( "boxFQN" ); + transpiler.setProperty( "boxClassName", boxClassName ); + String mappingName = transpiler.getProperty( "mappingName" ); + String mappingPath = transpiler.getProperty( "mappingPath" ); + String relativePath = transpiler.getProperty( "relativePath" ); + + Type type = Type.getType( "L" + transpiler.getProperty( "packageName" ).replace( '.', '/' ) + + "/" + transpiler.getProperty( "classname" ) + ";" ); + transpiler.setProperty( "classType", type.getDescriptor() ); + transpiler.setProperty( "classTypeInternal", type.getInternalName() ); + + List interfaces = new ArrayList<>(); + interfaces.add( Type.getType( IClassRunnable.class ) ); + interfaces.add( Type.getType( IReferenceable.class ) ); + interfaces.add( Type.getType( IType.class ) ); + interfaces.add( Type.getType( Serializable.class ) ); + List interfaceMethods = List.of(); + BoxExpression implementsValue = boxClass.getAnnotations().stream() + .filter( it -> it.getKey().getValue().equalsIgnoreCase( "implements" ) ) + .findFirst() + .map( BoxAnnotation::getValue ) + .orElse( null ); + if ( implementsValue instanceof BoxStringLiteral str ) { + String implementsStringList = str.getValue(); + // Collect and trim all strings starting with "java:" + Array implementsArray = ListUtil.asList( implementsStringList, "," ).stream() + .map( String::valueOf ) + .map( String::trim ) + .filter( it -> it.toLowerCase().startsWith( "java:" ) ) + .map( it -> it.substring( 5 ) ) + .collect( BLCollector.toArray() ); + var interfaceProxyDefinition = InterfaceProxyService.generateDefinition( new ScriptingRequestBoxContext(), implementsArray ); + // TODO: Remove methods that already have a @overrideJava UDF definition to avoid duplicates + interfaces.addAll( interfaceProxyDefinition.interfaces().stream().map( iface -> Type.getType( "L" + iface.replace( '.', '/' ) + ";" ) ).toList() ); + interfaceMethods = interfaceProxyDefinition.methods().stream() + .map( method -> AsmHelper.dereferenceAndInvoke( method.getName(), Type.getType( method ), type ) ) + .toList(); + } + + Type superclass = Type.getType( Object.class ); + boolean isJavaExtends; + List extendsMethods = List.of(); + BoxExpression extendsValue = boxClass.getAnnotations().stream() + .filter( it -> it.getKey().getValue().equalsIgnoreCase( "extends" ) ) + .findFirst() + .map( BoxAnnotation::getValue ) + .orElse( null ); + if ( extendsValue instanceof BoxStringLiteral str ) { + String extendsStringValue = str.getValue().trim(); + if ( extendsStringValue.toLowerCase().startsWith( "java:" ) ) { + superclass = Type.getType( "L" + extendsStringValue.substring( 5 ).replace( '.', '/' ) + ";" ); + isJavaExtends = true; + // search for UDFs that need a proxy created + extendsMethods = boxClass.getDescendantsOfType( BoxFunctionDeclaration.class ) + .stream() + .filter( it -> it.getAnnotations().stream().anyMatch( anno -> anno.getKey().getValue().equalsIgnoreCase( EXTENDS_ANNOTATION_MARKER ) ) ) + .map( func -> { + BoxReturnType boxReturnType = func.getType(); + BoxType boxType = BoxType.Any; + String fqn = null; + if ( boxReturnType != null ) { + boxType = boxReturnType.getType(); + if ( boxType.equals( BoxType.Fqn ) ) { + fqn = boxReturnType.getFqn(); + } + } + String returnTypeString = ( boxType.equals( BoxType.Fqn ) ? fqn : boxType.getSymbol() ); + if ( returnTypeString.equalsIgnoreCase( "Object" ) ) { + returnTypeString = "java.lang.Object"; + } + // Type returnType = Type + // .getType( "L" + ( boxType.equals( BoxType.Fqn ) ? fqn : boxType.getSymbol() ).replace( '.', '/' ) + ";" ); + // TODO this needs to be improved substantially + Type returnType = switch ( returnTypeString ) { + case "void" -> Type.VOID_TYPE; + case "long" -> Type + .getType( Long.class ); + default -> Type.getType( "L" + + returnTypeString.replace( '.', + '/' ) + + ";" ); + }; + List parameters = func.getArgs(); + Type[] parameterTypes = new Type[ parameters.size() ]; + for ( int i = 0; i < parameters.size(); i++ ) { + BoxArgumentDeclaration parameter = parameters.get( i ); + parameterTypes[ i ] = Type.getType( "L" + parameter.getType().replace( '.', '/' ) + ";" ); + + } + return AsmHelper.dereferenceAndInvoke( func.getName(), Type.getMethodType( returnType, parameterTypes ), type ); + } ) + .toList(); + } else { + isJavaExtends = false; + } + } else { + isJavaExtends = false; + } + + ClassNode classNode = new ClassNode(); + + AsmHelper.init( classNode, false, type, superclass, methodVisitor -> { + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ClassVariablesScope.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( ClassVariablesScope.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IClassRunnable.class ) ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "variablesScope", Type.getDescriptor( VariablesScope.class ) ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ThisScope.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( ThisScope.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "thisScope", Type.getDescriptor( ThisScope.class ) ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + transpiler.createKey( boxClassName ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "name", Type.getDescriptor( Key.class ) ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ArrayList.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, Type.getInternalName( ArrayList.class ), "", Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTFIELD, type.getInternalName(), "interfaces", Type.getDescriptor( List.class ) ); + + }, interfaces.toArray( Type[]::new ) ); + + interfaceMethods.forEach( methodNode -> methodNode.accept( classNode ) ); + extendsMethods.forEach( methodNode -> methodNode.accept( classNode ) ); + + classNode.visitField( Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, "serialVersionUID", Type.getDescriptor( long.class ), null, 1L ) + .visitEnd(); + + AsmHelper.addStaticFieldGetter( classNode, + type, + "imports", + "getImports", + Type.getType( List.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "path", + "getRunnablePath", + Type.getType( ResolvedFilePath.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "sourceType", + "getSourceType", + Type.getType( BoxSourceType.class ), + null ); + AsmHelper.addStaticFieldGetterWithStaticGetter( classNode, + type, + "annotations", + "getAnnotations", + "getAnnotationsStatic", + Type.getType( IStruct.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "documentation", + "getDocumentation", + Type.getType( IStruct.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "properties", + "getProperties", + Type.getType( Map.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "getterLookup", + "getGetterLookup", + Type.getType( Map.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "setterLookup", + "getSetterLookup", + Type.getType( Map.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "isJavaExtends", + "isJavaExtends", + Type.BOOLEAN_TYPE, + isJavaExtends ? 1 : 0 ); + AsmHelper.addStaticFieldGetterWithStaticGetter( classNode, + type, + "staticScope", + "getStaticScope", + "getStaticScopeStatic", + Type.getType( StaticScope.class ), + null ); + + classNode.visitField( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, + "keys", + Type.getDescriptor( Key[].class ), + null, + null ).visitEnd(); + classNode.visitField( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, + "staticInitialized", + Type.getDescriptor( boolean.class ), + null, + 0 ).visitEnd(); + + // classNode.visitField( Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, + // "compileTimeMethodNames", + // Type.getDescriptor( Set.class ), + // null, + // null ).visitEnd(); + AsmHelper.addPrviateStaticFieldGetter( classNode, + type, + "compileTimeMethodNames", + "getCompileTimeMethodNames", + Type.getType( Set.class ), + null ); + + AsmHelper.addPrviateStaticFieldGetter( classNode, + type, + "abstractMethods", + "getAbstractMethods", + Type.getType( Map.class ), + null ); + // TODO this is on the right track but needs need to match the body of the java version + MethodVisitor getAllAbstractMethodsMethodVisitor = classNode.visitMethod( Opcodes.ACC_PUBLIC, + "getAllAbstractMethods", + Type.getMethodDescriptor( Type.getType( Map.class ) ), + null, + null ); + getAllAbstractMethodsMethodVisitor.visitCode(); + getAllAbstractMethodsMethodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + getAllAbstractMethodsMethodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "abstractMethods", + Type.getType( Map.class ).getDescriptor() ); + getAllAbstractMethodsMethodVisitor.visitInsn( Type.getType( Map.class ).getOpcode( Opcodes.IRETURN ) ); + getAllAbstractMethodsMethodVisitor.visitMaxs( 0, 0 ); + getAllAbstractMethodsMethodVisitor.visitEnd(); + + defineLookupPrivateMethod( transpiler, classNode, type ); + defineLookupPrivateField( transpiler, classNode, type ); + + AsmHelper.addFieldGetter( classNode, + type, + "variablesScope", + "getVariablesScope", + Type.getType( VariablesScope.class ), + null ); + AsmHelper.addFieldGetter( classNode, + type, + "thisScope", + "getThisScope", + Type.getType( ThisScope.class ), + null ); + AsmHelper.addFieldGetter( classNode, + type, + "name", + "getName", + Type.getType( Key.class ), + null ); + AsmHelper.addFieldGetter( classNode, + type, + "interfaces", + "getInterfaces", + Type.getType( List.class ), + null ); + AsmHelper.addFieldGetterAndSetter( classNode, + type, + "_super", + "getSuper", + "_setSuper", + Type.getType( IClassRunnable.class ), + null, + methodVisitor -> { + } ); + AsmHelper.addFieldGetterAndSetter( classNode, + type, + "child", + "getChild", + "setChild", + Type.getType( IClassRunnable.class ), + null, + methodVisitor -> { + } ); + AsmHelper.addFieldGetterAndSetter( classNode, + type, + "canOutput", + "getCanOutput", + "setCanOutput", + Type.getType( Boolean.class ), + null, + methodVisitor -> { + } ); + AsmHelper.addFieldGetterAndSetter( classNode, + type, + "$bx", + "_getbx", + "_setbx", + Type.getType( BoxMeta.class ), + null, + methodVisitor -> { + } ); + AsmHelper.addFieldGetterAndSetter( classNode, + type, + "canInvokeImplicitAccessor", + "getCanInvokeImplicitAccessor", + "setCanInvokeImplicitAccessor", + Type.getType( Boolean.class ), + null, + methodVisitor -> { + } ); + + AsmHelper.boxClassSupport( classNode, "pseudoConstructor", Type.VOID_TYPE, Type.getType( IBoxContext.class ) ); + AsmHelper.boxClassSupport( classNode, "canOutput", Type.getType( Boolean.class ) ); + AsmHelper.boxClassSupport( classNode, "getBoxMeta", Type.getType( BoxMeta.class ) ); + AsmHelper.boxClassSupport( classNode, "getMetaData", Type.getType( IStruct.class ) ); + AsmHelper.boxClassSupport( classNode, "asString", Type.getType( String.class ) ); + AsmHelper.boxClassSupport( classNode, "canInvokeImplicitAccessor", Type.getType( Boolean.class ), Type.getType( IBoxContext.class ) ); + AsmHelper.boxClassSupport( classNode, "setSuper", Type.VOID_TYPE, Type.getType( IClassRunnable.class ) ); + AsmHelper.boxClassSupport( classNode, "getBottomClass", Type.getType( IClassRunnable.class ) ); + AsmHelper.boxClassSupport( classNode, "assign", Type.getType( Object.class ), Type.getType( IBoxContext.class ), Type.getType( Key.class ), + Type.getType( Object.class ) ); + AsmHelper.boxClassSupport( classNode, "dereference", Type.getType( Object.class ), Type.getType( IBoxContext.class ), Type.getType( Key.class ), + Type.getType( Boolean.class ) ); + AsmHelper.boxClassSupport( classNode, "dereferenceAndInvoke", Type.getType( Object.class ), Type.getType( IBoxContext.class ), + Type.getType( Key.class ), Type.getType( Object[].class ), Type.getType( Boolean.class ) ); + AsmHelper.boxClassSupport( classNode, "dereferenceAndInvoke", Type.getType( Object.class ), Type.getType( IBoxContext.class ), + Type.getType( Key.class ), Type.getType( Map.class ), Type.getType( Boolean.class ) ); + AsmHelper.boxClassSupport( classNode, "registerInterface", Type.VOID_TYPE, Type.getType( BoxInterface.class ) ); + + // these imports need to happen before any methods are processed - the actual nodes will be used later on in the static init section + List> imports = new ArrayList<>(); + for ( BoxImport statement : boxClass.getImports() ) { + imports.add( transpiler.transform( statement, TransformerContext.NONE, ReturnValueContext.EMPTY ) ); + } + List importNodes = AsmHelper.array( Type.getType( ImportDefinition.class ), Stream.concat( + imports.stream(), + transpiler.getImports().stream().map( raw -> { + List nodes = new ArrayList<>(); + nodes.addAll( raw ); + nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( ImportDefinition.class ), + "parse", + Type.getMethodDescriptor( Type.getType( ImportDefinition.class ), Type.getType( String.class ) ), + false ) ); + return nodes; + } ) + ).filter( l -> l.size() > 0 ).toList() ); + // end import node setup + + AsmHelper.methodWithContextAndClassLocator( classNode, "_pseudoConstructor", Type.getType( IBoxContext.class ), Type.VOID_TYPE, false, transpiler, + false, + () -> { + List psuedoBody = boxClass.getBody() + .stream() + .sorted( ( a, b ) -> { + if ( a instanceof BoxFunctionDeclaration && ! ( b instanceof BoxFunctionDeclaration ) ) { + return -1; + } else if ( b instanceof BoxFunctionDeclaration && ! ( a instanceof BoxFunctionDeclaration ) ) { + return 1; + } + + return 0; + + } ) + .flatMap( statement -> transpiler.transform( statement, TransformerContext.NONE, ReturnValueContext.EMPTY ).stream() ) + .collect( Collectors.toList() ); + + psuedoBody.add( new VarInsnNode( Opcodes.ALOAD, 0 ) ); + psuedoBody.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); + + psuedoBody.add( + new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( BoxClassSupport.class ), + "defaultProperties", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IClassRunnable.class ), Type.getType( IBoxContext.class ) ), + false + ) + ); + + return psuedoBody; + } + ); + + AsmHelper.methodWithContextAndClassLocator( classNode, "staticInitializer", Type.getType( IBoxContext.class ), Type.VOID_TYPE, true, transpiler, false, + () -> { + List staticNodes = ( List ) transpiler.getBoxStaticInitializers() + .stream() + .map( ( staticInitializer ) -> { + if ( staticInitializer == null || staticInitializer.getBody().size() == 0 ) { + return new ArrayList(); + } + + return staticInitializer.getBody() + .stream() + .map( statement -> transpiler.transform( statement, TransformerContext.NONE ) ) + .flatMap( nodes -> nodes.stream() ) + .collect( Collectors.toList() ); + } ) + .flatMap( s -> s.stream() ) + .collect( Collectors.toList() ); + + boxClass.getDescendantsOfType( BoxFunctionDeclaration.class, ( expr ) -> { + BoxFunctionDeclaration func = ( BoxFunctionDeclaration ) expr; + + return func.getModifiers().contains( BoxMethodDeclarationModifier.STATIC ); + } ).forEach( func -> { + staticNodes.addAll( transpiler.transform( func, TransformerContext.NONE ) ); + } ); + + return staticNodes; + } + ); + + AsmHelper.complete( classNode, type, methodVisitor -> { + AsmHelper.resolvedFilePath( methodVisitor, mappingName, mappingPath, relativePath, filePath ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "path", + Type.getDescriptor( ResolvedFilePath.class ) ); + + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + Type.getInternalName( BoxSourceType.class ), + sourceType, + Type.getDescriptor( BoxSourceType.class ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "sourceType", + Type.getDescriptor( BoxSourceType.class ) ); + + List annotations = transpiler.transformAnnotations( boxClass.getAnnotations() ); + List documenation = transpiler.transformDocumentation( boxClass.getDocumentation() ); + List> properties = transpiler.transformProperties( type, boxClass.getProperties(), sourceType ); + + methodVisitor.visitLdcInsn( transpiler.getKeys().size() ); + methodVisitor.visitTypeInsn( Opcodes.ANEWARRAY, Type.getInternalName( Key.class ) ); + int index = 0; + for ( BoxExpression expression : transpiler.getKeys().values() ) { + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitLdcInsn( index++ ); + transpiler.transform( expression, TransformerContext.NONE, ReturnValueContext.EMPTY ) + .forEach( methodInsnNode -> methodInsnNode.accept( methodVisitor ) ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, + Type.getInternalName( Key.class ), + "of", + Type.getMethodDescriptor( Type.getType( Key.class ), Type.getType( Object.class ) ), + false ); + methodVisitor.visitInsn( Opcodes.AASTORE ); + } + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "keys", + Type.getDescriptor( Key[].class ) ); + + methodVisitor.visitLdcInsn( 0 ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "staticInitialized", + Type.getDescriptor( boolean.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( StaticScope.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( StaticScope.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "staticScope", + Type.getDescriptor( StaticScope.class ) ); + + methodVisitor.visitLdcInsn( isJavaExtends ? 1 : 0 ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "isJavaExtends", Type.getDescriptor( boolean.class ) ); + + methodVisitor.visitLdcInsn( 1L ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "serialVersionUID", Type.getDescriptor( long.class ) ); + + importNodes.forEach( node -> node.accept( methodVisitor ) ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, + Type.getInternalName( List.class ), + "of", + Type.getMethodDescriptor( Type.getType( List.class ), Type.getType( Object[].class ) ), + true ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "imports", + Type.getDescriptor( List.class ) ); + + annotations.forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "annotations", + Type.getDescriptor( IStruct.class ) ); + + documenation.forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "documentation", + Type.getDescriptor( IStruct.class ) ); + + properties.get( 0 ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "properties", + Type.getDescriptor( Map.class ) ); + + properties.get( 1 ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "getterLookup", + Type.getDescriptor( Map.class ) ); + + properties.get( 2 ).forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "setterLookup", + Type.getDescriptor( Map.class ) ); + + generateSetOfCompileTimeMethodNames( transpiler, boxClass ).forEach( node -> node.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "compileTimeMethodNames", Type.getDescriptor( Set.class ) ); + + AsmHelper.generateMapOfAbstractMethodNames( transpiler, boxClass ).forEach( node -> node.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "abstractMethods", Type.getDescriptor( Map.class ) ); + } ); + + return classNode; + } + + private static List generateSetOfCompileTimeMethodNames( Transpiler transpiler, BoxClass boxClass ) { + List> methodKeyLists = boxClass.getDescendantsOfType( BoxFunctionDeclaration.class ) + .stream() + .map( BoxFunctionDeclaration::getName ) + .map( transpiler::createKey ) + .collect( java.util.stream.Collectors.toList() ); + + List nodes = new ArrayList(); + + nodes.addAll( AsmHelper.array( Type.getType( Key.class ), methodKeyLists ) ); + nodes.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName( Set.class ), + "of", + Type.getMethodDescriptor( Type.getType( Set.class ), Type.getType( Object[].class ) ), + true + ) + ); + + return nodes; + + } + + private static void defineLookupPrivateMethod( Transpiler transpiler, ClassNode classNode, Type thisType ) { + /** + * This code MUST be inside the class to allow for the lookupPrivate method to work + * This proxy is called from the dynamic interop service when calling a super method + * while using java extends, and it will return the method handle for the corresponding + * method in the super class. + */ + // public MethodHandle lookupPrivateMethod( Method method ) { + // try { + // return MethodHandles.lookup().findSpecial( + // method.getDeclaringClass(), + // method.getName(), + // MethodType.methodType(method.getReturnType(), method.getParameterTypes()), + // this.getClass() + // ); + // } catch (NoSuchMethodException | IllegalAccessException e) { + // throw new BoxRuntimeException( "Error getting Java super class method " + method.getName(), e ); + // } + // } + Type returnType = Type.getType( MethodHandle.class ); + MethodVisitor methodVisitor = classNode.visitMethod( + Opcodes.ACC_PUBLIC, + "lookupPrivateMethod", + Type.getMethodDescriptor( returnType, Type.getType( Method.class ) ), + null, + null ); + methodVisitor.visitCode(); + + Label tryStart = new Label(); + + methodVisitor.visitLabel( tryStart ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getType( MethodHandles.class ).getInternalName(), + "lookup", + Type.getMethodDescriptor( Type.getType( Lookup.class ) ), + false + ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + Type.getType( Member.class ).getInternalName(), + "getDeclaringClass", + Type.getMethodDescriptor( CLASS_TYPE ), + true + ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + Type.getType( Member.class ).getInternalName(), + "getName", + Type.getMethodDescriptor( Type.getType( String.class ) ), + true + ); + + // start MethodType.methodType(method.getReturnType(), method.getParameterTypes()), + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getType( Method.class ).getInternalName(), + "getReturnType", + Type.getMethodDescriptor( CLASS_TYPE ), + false + ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getType( Method.class ).getInternalName(), + "getParameterTypes", + Type.getMethodDescriptor( CLASS_ARRAY_TYPE ), + false + ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getType( MethodType.class ).getInternalName(), + "methodType", + Type.getMethodDescriptor( Type.getType( MethodType.class ), CLASS_TYPE, CLASS_ARRAY_TYPE ), + false + ); + // end MethodType.methodType(method.getReturnType(), method.getParameterTypes()), + + methodVisitor.visitLdcInsn( thisType ); + // methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, thisType.getInternalName(), "class", CLASS_TYPE.getDescriptor() ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getType( Lookup.class ).getInternalName(), + "findSpecial", + Type.getMethodDescriptor( Type.getType( MethodHandle.class ), CLASS_TYPE, Type.getType( String.class ), Type.getType( MethodType.class ), + CLASS_TYPE ), + false + ); + + methodVisitor.visitInsn( Opcodes.ARETURN ); + + Label tryEnd = new Label(); + Label handler = new Label(); + + methodVisitor.visitLabel( tryEnd ); + methodVisitor.visitLabel( handler ); + + methodVisitor.visitVarInsn( Opcodes.ASTORE, 2 ); + + methodVisitor.visitTryCatchBlock( tryStart, tryEnd, handler, Type.getInternalName( NoSuchMethodException.class ) ); + methodVisitor.visitTryCatchBlock( tryStart, tryEnd, handler, Type.getInternalName( IllegalAccessException.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( BoxRuntimeException.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + + methodVisitor.visitLdcInsn( "Error getting Java super class method " ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + Type.getType( Member.class ).getInternalName(), + "getName", + Type.getMethodDescriptor( Type.getType( String.class ) ), + true + ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getInternalName( String.class ), + "concat", + Type.getMethodDescriptor( Type.getType( String.class ), Type.getType( String.class ) ), + false + ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 2 ); + + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( BoxRuntimeException.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( String.class ), Type.getType( Throwable.class ) ), + false ); + + methodVisitor.visitInsn( Opcodes.ATHROW ); + + methodVisitor.visitMaxs( 0, 0 ); + + methodVisitor.visitEnd(); + } + + private static void defineLookupPrivateField( Transpiler transpiler, ClassNode classNode, Type thisType ) { + /** + * Same as above + */ + // public MethodHandle lookupPrivateField( Field field ) { + // try { + // return MethodHandles.lookup().unreflectGetter( field ); + // } catch ( IllegalAccessException e) { + // throw new BoxRuntimeException( "Error getting Java super class field " + field.getName(), e ); + // } + // } + Type returnType = Type.getType( MethodHandle.class ); + MethodVisitor methodVisitor = classNode.visitMethod( + Opcodes.ACC_PUBLIC, + "lookupPrivateField", + Type.getMethodDescriptor( returnType, Type.getType( Field.class ) ), + null, + null ); + methodVisitor.visitCode(); + + Label tryStart = new Label(); + + methodVisitor.visitLabel( tryStart ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getType( MethodHandles.class ).getInternalName(), + "lookup", + Type.getMethodDescriptor( Type.getType( Lookup.class ) ), + false + ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getType( Lookup.class ).getInternalName(), + "unreflectGetter", + Type.getMethodDescriptor( Type.getType( MethodHandle.class ), Type.getType( Field.class ) ), + false + ); + + methodVisitor.visitInsn( Opcodes.ARETURN ); + + Label tryEnd = new Label(); + Label handler = new Label(); + + methodVisitor.visitLabel( tryEnd ); + methodVisitor.visitLabel( handler ); + + methodVisitor.visitVarInsn( Opcodes.ASTORE, 2 ); + + methodVisitor.visitTryCatchBlock( tryStart, tryEnd, handler, Type.getInternalName( NoSuchMethodException.class ) ); + methodVisitor.visitTryCatchBlock( tryStart, tryEnd, handler, Type.getInternalName( IllegalAccessException.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( BoxRuntimeException.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + + methodVisitor.visitLdcInsn( "Error getting Java super class field " ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + Type.getType( Field.class ).getInternalName(), + "getName", + Type.getMethodDescriptor( Type.getType( String.class ) ), + true + ); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getInternalName( String.class ), + "concat", + Type.getMethodDescriptor( Type.getType( String.class ), Type.getType( String.class ) ), + false + ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 2 ); + + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( BoxRuntimeException.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( String.class ), Type.getType( Throwable.class ) ), + false ); + + methodVisitor.visitInsn( Opcodes.ATHROW ); + + methodVisitor.visitMaxs( 0, 0 ); + + methodVisitor.visitEnd(); + } + +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxComponentTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxComponentTransformer.java index 950007e80..8e6a5c43c 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxComponentTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxComponentTransformer.java @@ -10,6 +10,8 @@ import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.TypeInsnNode; @@ -43,6 +45,8 @@ public List transform( BoxNode node, TransformerContext contex throw new IllegalStateException(); } + transpiler.incrementComponentCounter(); + MethodContextTracker tracker = trackerOption.get(); List nodes = new ArrayList<>(); nodes.addAll( tracker.loadCurrentContext() ); @@ -51,7 +55,7 @@ public List transform( BoxNode node, TransformerContext contex nodes.addAll( transpiler.createKey( boxComponent.getName() ) ); // convert attributes to struct - nodes.addAll( transpiler.transformAnnotations( boxComponent.getAttributes() ) ); + nodes.addAll( transpiler.transformAnnotations( boxComponent.getAttributes(), true, false ) ); // Component.ComponentBody nodes.addAll( generateBodyNodes( boxComponent.getBody() ) ); @@ -62,7 +66,48 @@ public List transform( BoxNode node, TransformerContext contex Type.getMethodDescriptor( Type.getType( Component.BodyResult.class ), Type.getType( Key.class ), Type.getType( IStruct.class ), Type.getType( Component.ComponentBody.class ) ), true ) ); + + if ( boxComponent.getBody() == null || boxComponent.getBody().size() == 0 ) { + nodes.add( new InsnNode( Opcodes.POP ) ); + + transpiler.decrementComponentCounter(); + + return nodes; + } + + if ( transpiler.canReturn() ) { + LabelNode ifLabel = new LabelNode(); + + nodes.add( new InsnNode( Opcodes.DUP ) ); + + nodes.add( + new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, + Type.getInternalName( Component.BodyResult.class ), + "isEarlyExit", + Type.getMethodDescriptor( Type.BOOLEAN_TYPE ), + false + ) + ); + + nodes.add( new JumpInsnNode( Opcodes.IFEQ, ifLabel ) ); + + nodes.add( + new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, + Type.getInternalName( Component.BodyResult.class ), + "returnValue", + Type.getMethodDescriptor( Type.getType( Object.class ) ), + false + ) + ); + + nodes.add( new InsnNode( Opcodes.ARETURN ) ); + + nodes.add( ifLabel ); + } nodes.add( new InsnNode( Opcodes.POP ) ); + transpiler.decrementComponentCounter(); return nodes; } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxDoTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxDoTransformer.java index 789d1356e..c668f9de6 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxDoTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxDoTransformer.java @@ -48,10 +48,10 @@ public List transform( BoxNode node, TransformerContext contex LabelNode continueLabel = new LabelNode(); List nodes = new ArrayList<>(); - transpiler.setCurrentBreak( boxDo.getLabel(), end ); - transpiler.setCurrentBreak( null, end ); + transpiler.getCurrentMethodContextTracker().get().setCurrentBreak( boxDo.getLabel(), end ); + transpiler.getCurrentMethodContextTracker().get().setCurrentBreak( null, end ); - transpiler.setCurrentContinue( null, continueLabel ); + transpiler.getCurrentMethodContextTracker().get().setCurrentContinue( null, continueLabel ); // push two nulls onto the stack in order to initialize our strategy for keeping the stack height consistent // this is to allow the statement to return an expression in the case of a BoxScript execution diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForInTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForInTransformer.java index e8ecf67fe..fe68e654c 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForInTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForInTransformer.java @@ -30,6 +30,7 @@ import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.AsmTranspiler; import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker; import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker.VarStore; @@ -54,9 +55,10 @@ public BoxForInTransformer( Transpiler transpiler ) { } public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnValueContext ) { - BoxForIn forIn = ( BoxForIn ) node; - List nodes = new ArrayList<>(); - Optional trackerOption = transpiler.getCurrentMethodContextTracker(); + BoxForIn forIn = ( BoxForIn ) node; + List nodes = new ArrayList<>(); + AsmHelper.addDebugLabel( nodes, "BoxForIn" ); + Optional trackerOption = transpiler.getCurrentMethodContextTracker(); if ( trackerOption.isEmpty() ) { throw new IllegalStateException(); @@ -66,9 +68,16 @@ public List transform( BoxNode node, TransformerContext contex LabelNode loopStart = new LabelNode(); LabelNode loopEnd = new LabelNode(); + LabelNode breakTarget = new LabelNode(); + + tracker.setContinue( forIn, loopStart ); + tracker.setBreak( forIn, breakTarget ); + if ( forIn.getLabel() != null ) { + tracker.setStringLabel( forIn.getLabel(), forIn ); + } // access the collection - nodes.addAll( transpiler.transform( forIn.getExpression(), context ) ); + nodes.addAll( transpiler.transform( forIn.getExpression(), context, ReturnValueContext.VALUE_OR_NULL ) ); // unwrap it nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, Type.getInternalName( DynamicObject.class ), @@ -157,6 +166,13 @@ public List transform( BoxNode node, TransformerContext contex nodes.addAll( transpiler.transform( forIn.getBody(), context, returnValueContext ) ); nodes.add( new JumpInsnNode( Opcodes.GOTO, loopStart ) ); + + nodes.add( breakTarget ); + // every iteration we will swap the values and pop in order to remove the older value + if ( returnValueContext == ReturnValueContext.VALUE_OR_NULL ) { + nodes.add( new InsnNode( Opcodes.SWAP ) ); + nodes.add( new InsnNode( Opcodes.POP ) ); + } // increment query loop nodes.add( loopEnd ); diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForIndexTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForIndexTransformer.java index d2fe576a4..3c05b4f32 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForIndexTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxForIndexTransformer.java @@ -28,6 +28,7 @@ import org.objectweb.asm.tree.MethodInsnNode; import javassist.bytecode.Opcode; +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; @@ -45,26 +46,30 @@ public BoxForIndexTransformer( Transpiler transpiler ) { public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnValueContext ) { - BoxForIndex forIn = ( BoxForIndex ) node; - List nodes = new ArrayList<>(); - Optional trackerOption = transpiler.getCurrentMethodContextTracker(); + BoxForIndex forIn = ( BoxForIndex ) node; + List nodes = new ArrayList<>(); + AsmHelper.addDebugLabel( nodes, "BoxForIndex" ); + Optional trackerOption = transpiler.getCurrentMethodContextTracker(); if ( trackerOption.isEmpty() ) { throw new IllegalStateException(); } - LabelNode breakTarget = new LabelNode(); - LabelNode firstLoop = new LabelNode(); - LabelNode loopStart = new LabelNode(); - LabelNode loopEnd = new LabelNode(); + MethodContextTracker tracker = trackerOption.get(); - transpiler.setCurrentBreak( forIn.getLabel(), breakTarget ); - transpiler.setCurrentBreak( null, breakTarget ); + LabelNode breakTarget = new LabelNode(); + LabelNode firstLoop = new LabelNode(); + LabelNode loopStart = new LabelNode(); + LabelNode loopEnd = new LabelNode(); - transpiler.setCurrentContinue( null, loopStart ); - transpiler.setCurrentContinue( forIn.getLabel(), loopStart ); + tracker.setContinue( forIn, loopStart ); + tracker.setBreak( forIn, breakTarget ); + if ( forIn.getLabel() != null ) { + tracker.setStringLabel( forIn.getLabel(), forIn ); + } if ( forIn.getInitializer() != null ) { + AsmHelper.addDebugLabel( nodes, "BoxForIndex - initializer" ); nodes.addAll( transpiler.transform( forIn.getInitializer(), context, ReturnValueContext.EMPTY ) ); } @@ -77,17 +82,21 @@ public List transform( BoxNode node, TransformerContext contex nodes.add( new JumpInsnNode( Opcode.GOTO, firstLoop ) ); + AsmHelper.addDebugLabel( nodes, "BoxForIndex - loopStart" ); nodes.add( loopStart ); if ( forIn.getStep() != null ) { + AsmHelper.addDebugLabel( nodes, "BoxForIndex - step" ); nodes.addAll( transpiler.transform( forIn.getStep(), context, ReturnValueContext.EMPTY ) ); } + AsmHelper.addDebugLabel( nodes, "BoxForIndex - firstLoop" ); nodes.add( firstLoop ); nodes.add( new InsnNode( Opcodes.SWAP ) ); nodes.add( new InsnNode( Opcodes.POP ) ); + AsmHelper.addDebugLabel( nodes, "BoxForIndex - condition" ); if ( forIn.getCondition() != null ) { nodes.addAll( transpiler.transform( forIn.getCondition(), context, ReturnValueContext.VALUE ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, @@ -107,21 +116,30 @@ public List transform( BoxNode node, TransformerContext contex nodes.add( new JumpInsnNode( Opcodes.IFEQ, loopEnd ) ); + AsmHelper.addDebugLabel( nodes, "BoxForIndex - body" ); nodes.addAll( transpiler.transform( forIn.getBody(), context, ReturnValueContext.VALUE_OR_NULL ) ); nodes.add( new JumpInsnNode( Opcode.GOTO, loopStart ) ); + AsmHelper.addDebugLabel( nodes, "BoxForIndex - breakTarget" ); nodes.add( breakTarget ); nodes.add( new InsnNode( Opcodes.SWAP ) ); nodes.add( new InsnNode( Opcodes.POP ) ); + AsmHelper.addDebugLabel( nodes, "BoxForIndex - loopEnd" ); nodes.add( loopEnd ); if ( returnValueContext.empty ) { nodes.add( new InsnNode( Opcodes.POP ) ); } + tracker.setCurrentBreak( null, null ); + + tracker.setCurrentContinue( null, null ); + + AsmHelper.addDebugLabel( nodes, "BoxForIndex - done" ); + return nodes; } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxFunctionDeclarationTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxFunctionDeclarationTransformer.java index 04bb239fc..46e56deff 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxFunctionDeclarationTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxFunctionDeclarationTransformer.java @@ -14,14 +14,16 @@ */ package ortus.boxlang.compiler.asmboxpiler.transformer.statement; +import java.util.ArrayList; import java.util.List; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.VarInsnNode; import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.AsmTranspiler; @@ -31,6 +33,7 @@ import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.statement.BoxAccessModifier; import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxMethodDeclarationModifier; import ortus.boxlang.compiler.ast.statement.BoxReturnType; import ortus.boxlang.compiler.ast.statement.BoxType; import ortus.boxlang.compiler.parser.BoxSourceType; @@ -77,6 +80,14 @@ public List transform( BoxNode node, TransformerContext contex } ); transpiler.setAuxiliary( type.getClassName(), classNode ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "modifiers", + "getModifiers", + Type.getType( List.class ), + null + ); + AsmHelper.addStaticFieldGetter( classNode, type, "name", @@ -133,10 +144,21 @@ public List transform( BoxNode node, TransformerContext contex "getSourceType", Type.getType( BoxSourceType.class ) ); + transpiler.incrementfunctionBodyCounter(); AsmHelper.methodWithContextAndClassLocator( classNode, "_invoke", Type.getType( FunctionBoxContext.class ), Type.getType( Object.class ), false, transpiler, true, - () -> function.getBody().stream().flatMap( statement -> transpiler.transform( statement, safe, ReturnValueContext.EMPTY ).stream() ) - .toList() ); + () -> { + + if ( function.getBody() == null ) { + return new ArrayList(); + } + + return function.getBody() + .stream() + .flatMap( statement -> transpiler.transform( statement, safe, ReturnValueContext.EMPTY ).stream() ) + .toList(); + } ); + transpiler.decrementfunctionBodyCounter(); AsmHelper.complete( classNode, type, methodVisitor -> { transpiler.createKey( function.getName() ).forEach( methodInsnNode -> methodInsnNode.accept( methodVisitor ) ); @@ -174,20 +196,55 @@ public List transform( BoxNode node, TransformerContext contex type.getInternalName(), "documentation", Type.getDescriptor( IStruct.class ) ); + + AsmHelper.array( + Type.getType( BoxMethodDeclarationModifier.class ), + function.getModifiers(), + ( bmdm, i ) -> List.of( + new FieldInsnNode( + Opcodes.GETSTATIC, + Type.getInternalName( BoxMethodDeclarationModifier.class ), + bmdm.toString().toUpperCase(), + Type.getDescriptor( BoxMethodDeclarationModifier.class ) + ) + ) + ).stream() + .forEach( modifierNode -> modifierNode.accept( methodVisitor ) ); + + methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, + Type.getInternalName( List.class ), + "of", + Type.getMethodDescriptor( Type.getType( List.class ), Type.getType( Object[].class ) ), + true ); + + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "modifiers", + Type.getDescriptor( List.class ) ); } ); - return List.of( - new VarInsnNode( Opcodes.ALOAD, 1 ), + List nodes = new ArrayList(); + + nodes.addAll( transpiler.getCurrentMethodContextTracker().get().loadCurrentContext() ); + nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, type.getInternalName(), "getInstance", Type.getMethodDescriptor( type ), - false ), + false ) + ); + nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, Type.getInternalName( IBoxContext.class ), "registerUDF", Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( UDF.class ) ), true ) ); + + if ( returnContext.nullable ) { + nodes.add( new InsnNode( Opcodes.ACONST_NULL ) ); + } + + return nodes; } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxIfElseTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxIfElseTransformer.java index e806e107f..4c08827c8 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxIfElseTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxIfElseTransformer.java @@ -25,6 +25,7 @@ import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.MethodInsnNode; +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; @@ -44,6 +45,7 @@ public List transform( BoxNode node, TransformerContext contex BoxIfElse ifElse = ( BoxIfElse ) node; List nodes = new ArrayList<>(); + AsmHelper.addDebugLabel( nodes, "BoxIf" ); nodes.addAll( transpiler.transform( ifElse.getCondition(), TransformerContext.NONE, ReturnValueContext.VALUE ) ); nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, Type.getInternalName( BooleanCaster.class ), diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxInterfaceTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxInterfaceTransformer.java new file mode 100644 index 000000000..c3b768283 --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxInterfaceTransformer.java @@ -0,0 +1,510 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.statement; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.AsmHelper; +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxExpression; +import ortus.boxlang.compiler.ast.BoxInterface; +import ortus.boxlang.compiler.ast.BoxStaticInitializer; +import ortus.boxlang.compiler.ast.Source; +import ortus.boxlang.compiler.ast.SourceFile; +import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; +import ortus.boxlang.compiler.ast.statement.BoxImport; +import ortus.boxlang.compiler.ast.statement.BoxMethodDeclarationModifier; +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.InterfaceBoxContext; +import ortus.boxlang.runtime.loader.ImportDefinition; +import ortus.boxlang.runtime.runnables.IBoxRunnable; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.StaticScope; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.types.exceptions.ExpressionException; +import ortus.boxlang.runtime.util.ResolvedFilePath; + +public class BoxInterfaceTransformer { + + public static ClassNode transpile( Transpiler transpiler, BoxInterface boxInterface ) throws BoxRuntimeException { + + Source source = boxInterface.getPosition().getSource(); + String packageName = transpiler.getProperty( "packageName" ); + String boxPackageName = transpiler.getProperty( "boxFQN" ); + transpiler.setProperty( "boxClassName", boxPackageName ); + String classname = transpiler.getProperty( "classname" ); + String mappingName = transpiler.getProperty( "mappingName" ); + String mappingPath = transpiler.getProperty( "mappingPath" ); + String relativePath = transpiler.getProperty( "relativePath" ); + String fileName = source instanceof SourceFile file && file.getFile() != null ? file.getFile().getName() : "unknown"; + String filePath = source instanceof SourceFile file && file.getFile() != null ? file.getFile().getAbsolutePath() : "unknown"; + // trim leading . if exists + String boxInterfacename = transpiler.getProperty( "boxFQN" ); + String sourceType = transpiler.getProperty( "sourceType" ); + + Type type = Type.getType( "L" + packageName.replace( '.', '/' ) + + "/" + classname + ";" ); + transpiler.setProperty( "classType", type.getDescriptor() ); + transpiler.setProperty( "classTypeInternal", type.getInternalName() ); + + ClassNode classNode = new ClassNode(); + + AsmHelper.init( classNode, false, type, Type.getType( ortus.boxlang.runtime.runnables.BoxInterface.class ), methodVisitor -> { + } ); + + AsmHelper.addLazySingleton( classNode, type, methodVisitor -> { + methodVisitor.visitTypeInsn( Opcodes.NEW, type.getInternalName() ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + type.getInternalName(), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( StaticScope.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( StaticScope.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( ortus.boxlang.runtime.runnables.BoxInterface.class ) ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "staticScope", + Type.getDescriptor( StaticScope.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( InterfaceBoxContext.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 0 ); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( InterfaceBoxContext.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE, + Type.getType( IBoxContext.class ), + Type.getType( ortus.boxlang.runtime.runnables.BoxInterface.class ) ), + false ); + methodVisitor.visitVarInsn( Opcodes.ASTORE, 1 ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( InterfaceBoxContext.class ), + "pushTemplate", + Type.getMethodDescriptor( Type.getType( IBoxContext.class ), Type.getType( IBoxRunnable.class ) ), + false ); + methodVisitor.visitInsn( Opcodes.POP ); + + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( ortus.boxlang.runtime.runnables.BoxInterface.class ), + "resolveSupers", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IBoxContext.class ) ), + false ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, + type.getInternalName(), + "staticInitializer", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IBoxContext.class ) ), + false ); + + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "instance", + type.getDescriptor() ); + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL, + type.getInternalName(), + "pseudoConstructor", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IBoxContext.class ) ), + false ); + + methodVisitor.visitVarInsn( Opcodes.ALOAD, 1 ); + methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( InterfaceBoxContext.class ), + "popTemplate", + Type.getMethodDescriptor( Type.getType( ResolvedFilePath.class ) ), + false ); + methodVisitor.visitInsn( Opcodes.POP ); + }, Type.getType( IBoxContext.class ) ); + + AsmHelper.addStaticFieldGetter( classNode, + type, + "imports", + "getImports", + Type.getType( List.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "path", + "getRunnablePath", + Type.getType( ResolvedFilePath.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "sourceType", + "getSourceType", + Type.getType( BoxSourceType.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "annotations", + "getAnnotations", + Type.getType( IStruct.class ), + null ); + AsmHelper.addStaticFieldGetter( classNode, + type, + "documentation", + "getDocumentation", + Type.getType( IStruct.class ), + null ); + AsmHelper.addStaticFieldGetterWithStaticGetter( classNode, + type, + "staticScope", + "getStaticScope", + "getStaticScopeStatic", + Type.getType( StaticScope.class ), + null, + false ); + + AsmHelper.addStaticFieldGetter( classNode, + type, + "name", + "getName", + Type.getType( Key.class ), + null ); + + AsmHelper.addStaticFieldGetter( classNode, + type, + "_supers", + "getSupers", + Type.getType( List.class ), + null ); + MethodVisitor addSuper = classNode.visitMethod( Opcodes.ACC_PUBLIC, + "_addSuper", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( ortus.boxlang.runtime.runnables.BoxInterface.class ) ), + null, + null ); + addSuper.visitCode(); + addSuper.visitFieldInsn( Opcodes.GETSTATIC, + type.getInternalName(), + "_supers", + Type.getDescriptor( List.class ) ); + addSuper.visitVarInsn( Opcodes.ALOAD, 1 ); + addSuper.visitMethodInsn( Opcodes.INVOKEINTERFACE, + Type.getInternalName( List.class ), + "add", + Type.getMethodDescriptor( Type.BOOLEAN_TYPE, Type.getType( Object.class ) ), + true ); + addSuper.visitInsn( Opcodes.POP ); + addSuper.visitInsn( Opcodes.RETURN ); + addSuper.visitMaxs( 0, 0 ); + addSuper.visitEnd(); + + classNode.visitField( Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, + "keys", + Type.getDescriptor( Key[].class ), + null, + null ).visitEnd(); + + AsmHelper.addPrviateStaticFieldGetter( classNode, + type, + "abstractMethods", + "getAbstractMethods", + Type.getType( Map.class ), + null ); + AsmHelper.addPrviateStaticFieldGetter( classNode, + type, + "defaultMethods", + "getDefaultMethods", + Type.getType( Map.class ), + null ); + + Label start = new Label(), end = new Label(), handler = new Label(); + MethodVisitor pseudoConstructor = classNode.visitMethod( Opcodes.ACC_PUBLIC, + "pseudoConstructor", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IBoxContext.class ) ), + null, + null ); + pseudoConstructor.visitTryCatchBlock( start, end, handler, null ); + pseudoConstructor.visitCode(); + pseudoConstructor.visitVarInsn( Opcodes.ALOAD, 1 ); + pseudoConstructor.visitVarInsn( Opcodes.ALOAD, 0 ); + pseudoConstructor.visitMethodInsn( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "pushTemplate", + Type.getMethodDescriptor( Type.getType( IBoxContext.class ), Type.getType( IBoxRunnable.class ) ), + true ); + pseudoConstructor.visitInsn( Opcodes.POP ); + pseudoConstructor.visitLabel( start ); + pseudoConstructor.visitVarInsn( Opcodes.ALOAD, 0 ); + pseudoConstructor.visitVarInsn( Opcodes.ALOAD, 1 ); + pseudoConstructor.visitMethodInsn( Opcodes.INVOKEVIRTUAL, + type.getInternalName(), + "_pseudoConstructor", + Type.getMethodDescriptor( Type.VOID_TYPE, Type.getType( IBoxContext.class ) ), + false ); + pseudoConstructor.visitLabel( end ); + pseudoConstructor.visitVarInsn( Opcodes.ALOAD, 1 ); + pseudoConstructor.visitMethodInsn( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "popTemplate", + Type.getMethodDescriptor( Type.getType( ResolvedFilePath.class ) ), + true ); + pseudoConstructor.visitInsn( Opcodes.POP ); + pseudoConstructor.visitInsn( Opcodes.RETURN ); + pseudoConstructor.visitLabel( handler ); + pseudoConstructor.visitVarInsn( Opcodes.ALOAD, 1 ); + pseudoConstructor.visitMethodInsn( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "popTemplate", + Type.getMethodDescriptor( Type.getType( ResolvedFilePath.class ) ), + true ); + pseudoConstructor.visitInsn( Opcodes.POP ); + pseudoConstructor.visitInsn( Opcodes.ATHROW ); + pseudoConstructor.visitMaxs( 0, 0 ); + pseudoConstructor.visitEnd(); + + // these imports need to happen before any methods are processed - the actual nodes will be used later on in the static init section + List> imports = new ArrayList<>(); + for ( BoxImport statement : boxInterface.getImports() ) { + imports.add( transpiler.transform( statement, TransformerContext.NONE, ReturnValueContext.EMPTY ) ); + } + List importNodes = AsmHelper.array( Type.getType( ImportDefinition.class ), Stream.concat( + imports.stream(), + transpiler.getImports().stream().map( raw -> { + List nodes = new ArrayList<>(); + nodes.addAll( raw ); + nodes.add( new MethodInsnNode( Opcodes.INVOKESTATIC, + Type.getInternalName( ImportDefinition.class ), + "parse", + Type.getMethodDescriptor( Type.getType( ImportDefinition.class ), Type.getType( String.class ) ), + false ) ); + return nodes; + } ) + ).filter( l -> l.size() > 0 ).toList() ); + // end import node setup + + AsmHelper.methodWithContextAndClassLocator( classNode, "_pseudoConstructor", Type.getType( IBoxContext.class ), Type.VOID_TYPE, false, transpiler, + false, + () -> { + return boxInterface.getBody() + .stream() + .sorted( ( a, b ) -> { + if ( a instanceof BoxFunctionDeclaration && ! ( b instanceof BoxFunctionDeclaration ) ) { + return -1; + } else if ( b instanceof BoxFunctionDeclaration && ! ( a instanceof BoxFunctionDeclaration ) ) { + return 1; + } + + return 0; + + } ) + .flatMap( statement -> { + + if ( ! ( statement instanceof BoxFunctionDeclaration ) + && ! ( statement instanceof BoxImport ) + && ! ( statement instanceof BoxStaticInitializer ) ) { + throw new ExpressionException( + "Statement type not supported in an interface: " + statement.getClass().getSimpleName(), + statement + ); + } + + if ( statement instanceof BoxFunctionDeclaration bfd && bfd.getBody() == null ) { + return new ArrayList().stream(); + } + + return transpiler.transform( statement, TransformerContext.NONE, ReturnValueContext.EMPTY ).stream(); + } ) + .toList(); + } + ); + + AsmHelper.methodWithContextAndClassLocator( classNode, "staticInitializer", Type.getType( IBoxContext.class ), Type.VOID_TYPE, true, transpiler, false, + () -> { + List staticNodes = new ArrayList(); + + boxInterface.getDescendantsOfType( BoxFunctionDeclaration.class, ( expr ) -> { + BoxFunctionDeclaration func = ( BoxFunctionDeclaration ) expr; + + return func.getModifiers().contains( BoxMethodDeclarationModifier.STATIC ); + } ).forEach( func -> { + staticNodes.addAll( transpiler.transform( func, TransformerContext.NONE ) ); + } ); + + staticNodes.addAll( transpiler.getBoxStaticInitializers() + .stream() + .map( ( staticInitializer ) -> { + if ( staticInitializer == null || staticInitializer.getBody().size() == 0 ) { + return new ArrayList(); + } + + return staticInitializer.getBody() + .stream() + .map( statement -> transpiler.transform( statement, TransformerContext.NONE ) ) + .flatMap( nodes -> nodes.stream() ) + .collect( Collectors.toList() ); + } ) + .flatMap( s -> s.stream() ) + .collect( Collectors.toList() ) + ); + + return staticNodes; + } + ); + + AsmHelper.complete( classNode, type, methodVisitor -> { + AsmHelper.resolvedFilePath( methodVisitor, mappingName, mappingPath, relativePath, filePath ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "path", + Type.getDescriptor( ResolvedFilePath.class ) ); + + methodVisitor.visitFieldInsn( Opcodes.GETSTATIC, + Type.getInternalName( BoxSourceType.class ), + sourceType, + Type.getDescriptor( BoxSourceType.class ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "sourceType", + Type.getDescriptor( BoxSourceType.class ) ); + + List annotations = transpiler.transformAnnotations( boxInterface.getAllAnnotations() ); + List documenation = transpiler.transformDocumentation( boxInterface.getDocumentation() ); + List name = transpiler.createKey( boxInterfacename ); + + List abstractMethods = AsmHelper.generateMapOfAbstractMethodNames( transpiler, boxInterface ); + + methodVisitor.visitLdcInsn( transpiler.getKeys().size() ); + methodVisitor.visitTypeInsn( Opcodes.ANEWARRAY, Type.getInternalName( Key.class ) ); + int index = 0; + for ( BoxExpression expression : transpiler.getKeys().values() ) { + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitLdcInsn( index++ ); + transpiler.transform( expression, TransformerContext.NONE, ReturnValueContext.EMPTY ) + .forEach( methodInsnNode -> methodInsnNode.accept( methodVisitor ) ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, + Type.getInternalName( Key.class ), + "of", + Type.getMethodDescriptor( Type.getType( Key.class ), Type.getType( Object.class ) ), + false ); + methodVisitor.visitInsn( Opcodes.AASTORE ); + } + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "keys", + Type.getDescriptor( Key[].class ) ); + + name.forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "name", + Type.getDescriptor( Key.class ) ); + + importNodes.forEach( node -> node.accept( methodVisitor ) ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC, + Type.getInternalName( List.class ), + "of", + Type.getMethodDescriptor( Type.getType( List.class ), Type.getType( Object[].class ) ), + true ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "imports", + Type.getDescriptor( List.class ) ); + + annotations.forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "annotations", + Type.getDescriptor( IStruct.class ) ); + + documenation.forEach( abstractInsnNode -> abstractInsnNode.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, + type.getInternalName(), + "documentation", + Type.getDescriptor( IStruct.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( LinkedHashMap.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( LinkedHashMap.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "abstractMethods", Type.getDescriptor( Map.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( LinkedHashMap.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( LinkedHashMap.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "defaultMethods", Type.getDescriptor( Map.class ) ); + + methodVisitor.visitTypeInsn( Opcodes.NEW, Type.getInternalName( ArrayList.class ) ); + methodVisitor.visitInsn( Opcodes.DUP ); + methodVisitor.visitMethodInsn( Opcodes.INVOKESPECIAL, + Type.getInternalName( ArrayList.class ), + "", + Type.getMethodDescriptor( Type.VOID_TYPE ), + false ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "_supers", Type.getDescriptor( List.class ) ); + + abstractMethods.forEach( node -> node.accept( methodVisitor ) ); + methodVisitor.visitFieldInsn( Opcodes.PUTSTATIC, type.getInternalName(), "abstractMethods", Type.getDescriptor( Map.class ) ); + } ); + + return classNode; + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxParamTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxParamTransformer.java new file mode 100644 index 000000000..ee2ba04bb --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxParamTransformer.java @@ -0,0 +1,79 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.statement; + +import java.util.ArrayList; +import java.util.List; + +import org.objectweb.asm.tree.AbstractInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.AsmTranspiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.expression.BoxFQN; +import ortus.boxlang.compiler.ast.statement.BoxAnnotation; +import ortus.boxlang.compiler.ast.statement.BoxParam; +import ortus.boxlang.compiler.ast.statement.component.BoxComponent; + +public class BoxParamTransformer extends AbstractTransformer { + + public BoxParamTransformer( AsmTranspiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) { + BoxParam boxParam = ( BoxParam ) node; + List attrs = new ArrayList(); + attrs.add( + new BoxAnnotation( + new BoxFQN( "name", + boxParam.getVariable().getPosition(), + boxParam.getVariable().getSourceText() ), + boxParam.getVariable(), + boxParam.getVariable().getPosition(), + boxParam.getVariable().getSourceText() + ) + ); + if ( boxParam.getType() != null ) { + attrs.add( + new BoxAnnotation( + new BoxFQN( "type", + boxParam.getType().getPosition(), + boxParam.getType().getSourceText() ), + boxParam.getType(), + boxParam.getType().getPosition(), + boxParam.getType().getSourceText() + ) + ); + } + if ( boxParam.getDefaultValue() != null ) { + attrs.add( + new BoxAnnotation( + new BoxFQN( "default", + boxParam.getDefaultValue().getPosition(), + boxParam.getDefaultValue().getSourceText() ), + boxParam.getDefaultValue(), + boxParam.getDefaultValue().getPosition(), + boxParam.getDefaultValue().getSourceText() + ) + ); + } + // Delegate to the component transformer + return transpiler.transform( new BoxComponent( "param", attrs, node.getPosition(), node.getSourceText() ), context ); + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxScriptIslandTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxScriptIslandTransformer.java new file mode 100644 index 000000000..39e4c62c5 --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxScriptIslandTransformer.java @@ -0,0 +1,50 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.statement; + +import java.util.ArrayList; +import java.util.List; + +import org.objectweb.asm.tree.AbstractInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.BoxStatement; +import ortus.boxlang.compiler.ast.statement.BoxScriptIsland; + +public class BoxScriptIslandTransformer extends AbstractTransformer { + + public BoxScriptIslandTransformer( Transpiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { + BoxScriptIsland scriptIsland = ( BoxScriptIsland ) node; + + List nodes = new ArrayList<>(); + for ( BoxStatement statement : scriptIsland.getStatements() ) { + nodes.addAll( transpiler.transform( statement, context, returnContext ) ); + } + + return nodes; + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxStaticInitializerTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxStaticInitializerTransformer.java new file mode 100644 index 000000000..c137a057c --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxStaticInitializerTransformer.java @@ -0,0 +1,42 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.statement; + +import java.util.List; + +import org.objectweb.asm.tree.AbstractInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.BoxStaticInitializer; + +public class BoxStaticInitializerTransformer extends AbstractTransformer { + + public BoxStaticInitializerTransformer( Transpiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { + BoxStaticInitializer staticInitializer = ( BoxStaticInitializer ) node; + + transpiler.addBoxStaticInitializer( staticInitializer ); + + return List.of(); + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTemplateIslandTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTemplateIslandTransformer.java new file mode 100644 index 000000000..b9a2fbc76 --- /dev/null +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTemplateIslandTransformer.java @@ -0,0 +1,50 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.compiler.asmboxpiler.transformer.statement; + +import java.util.ArrayList; +import java.util.List; + +import org.objectweb.asm.tree.AbstractInsnNode; + +import ortus.boxlang.compiler.asmboxpiler.Transpiler; +import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; +import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; +import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; +import ortus.boxlang.compiler.ast.BoxNode; +import ortus.boxlang.compiler.ast.BoxStatement; +import ortus.boxlang.compiler.ast.statement.component.BoxTemplateIsland; + +public class BoxTemplateIslandTransformer extends AbstractTransformer { + + public BoxTemplateIslandTransformer( Transpiler transpiler ) { + super( transpiler ); + } + + @Override + public List transform( BoxNode node, TransformerContext context, ReturnValueContext returnContext ) throws IllegalStateException { + BoxTemplateIsland templateIsland = ( BoxTemplateIsland ) node; + + List nodes = new ArrayList<>(); + for ( BoxStatement statement : templateIsland.getStatements() ) { + nodes.addAll( transpiler.transform( statement, context, returnContext ) ); + } + + return nodes; + } +} diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTryTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTryTransformer.java index d192e485a..eade0d83f 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTryTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxTryTransformer.java @@ -91,7 +91,7 @@ public List transform( BoxNode node, TransformerContext contex TryCatchBlockNode catchHandler = new TryCatchBlockNode( tryStartLabel, tryEndLabel, javaCatchBodyStart, null ); - transpiler.addTryCatchBlock( catchHandler ); + tracker.addTryCatchBlock( catchHandler ); // if we are here none of our catch handlers matched the error so we inline another finally block if ( boxTry.getFinallyBody().size() == 0 ) { @@ -106,7 +106,7 @@ public List transform( BoxNode node, TransformerContext contex TryCatchBlockNode catchHandler = new TryCatchBlockNode( tryStartLabel, tryEndLabel, finallyStartLabel, null ); - transpiler.addTryCatchBlock( catchHandler ); + tracker.addTryCatchBlock( catchHandler ); nodes.add( finallyStartLabel ); @@ -121,7 +121,7 @@ public List transform( BoxNode node, TransformerContext contex nodes.add( finallyEndLabel ); - transpiler.addTryCatchBlock( new TryCatchBlockNode( tryStartLabel, tryEndLabel, finallyStartLabel, null ) ); + tracker.addTryCatchBlock( new TryCatchBlockNode( tryStartLabel, tryEndLabel, finallyStartLabel, null ) ); return nodes; @@ -140,6 +140,14 @@ private List generateBodyNodesWithInlinedFinally( if ( returnValueContext != ReturnValueContext.VALUE_OR_NULL ) { nodes.add( new InsnNode( Opcodes.POP ) ); } + + AbstractInsnNode inBetweenNode = inBetween.get(); + + if ( inBetweenNode != null ) { + nodes.add( inBetweenNode ); + } + + return nodes; } nodes.addAll( AsmHelper.transformBodyExpressions( @@ -213,7 +221,7 @@ private List generateCatchBodyNodes( nodes.add( new JumpInsnNode( Opcodes.GOTO, finallyEndLabel ) ); nodes.add( endHandlerLabel ); - transpiler.addTryCatchBlock( new TryCatchBlockNode( startHandlerLabel, endHandlerLabel, finallyStartLabel, null ) ); + tracker.addTryCatchBlock( new TryCatchBlockNode( startHandlerLabel, endHandlerLabel, finallyStartLabel, null ) ); return nodes; } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxWhileTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxWhileTransformer.java index 13ce37512..01e1c478b 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxWhileTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/statement/BoxWhileTransformer.java @@ -25,6 +25,7 @@ import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.MethodInsnNode; +import ortus.boxlang.compiler.asmboxpiler.MethodContextTracker; import ortus.boxlang.compiler.asmboxpiler.Transpiler; import ortus.boxlang.compiler.asmboxpiler.transformer.AbstractTransformer; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; @@ -48,14 +49,13 @@ public List transform( BoxNode node, TransformerContext contex breakTarget = new LabelNode(); List nodes = new ArrayList<>(); - // if ( boxWhile.getLabel() != null ) { + MethodContextTracker tracker = transpiler.getCurrentMethodContextTracker().get(); + tracker.setBreak( boxWhile, breakTarget ); + tracker.setContinue( boxWhile, start ); - // } - transpiler.setCurrentBreak( boxWhile.getLabel(), breakTarget ); - transpiler.setCurrentBreak( "", breakTarget ); - - transpiler.setCurrentContinue( null, start ); - transpiler.setCurrentContinue( boxWhile.getLabel(), start ); + if ( boxWhile.getLabel() != null ) { + tracker.setStringLabel( boxWhile.getLabel(), boxWhile ); + } // push two nulls onto the stack in order to initialize our strategy for keeping the stack height consistent // this is to allow the statement to return an expression in the case of a BoxScript execution diff --git a/src/main/java/ortus/boxlang/compiler/javaboxpiler/JavaBoxpiler.java b/src/main/java/ortus/boxlang/compiler/javaboxpiler/JavaBoxpiler.java index 11900b398..d7be6d9b4 100644 --- a/src/main/java/ortus/boxlang/compiler/javaboxpiler/JavaBoxpiler.java +++ b/src/main/java/ortus/boxlang/compiler/javaboxpiler/JavaBoxpiler.java @@ -181,7 +181,6 @@ public void compileClassInfo( String classPoolName, String FQN ) { File sourceFile = classInfo.resolvedFilePath().absolutePath().toFile(); // Check if the source file contains Java bytecode by reading the first few bytes if ( diskClassUtil.isJavaBytecode( sourceFile ) ) { - System.out.println( "Loading bytecode direct from pre-compiled source file for " + FQN ); classInfo.getClassLoader().defineClasses( FQN, sourceFile ); return; } diff --git a/src/main/java/ortus/boxlang/runtime/BoxRuntime.java b/src/main/java/ortus/boxlang/runtime/BoxRuntime.java index 64242967c..43f41a3c9 100644 --- a/src/main/java/ortus/boxlang/runtime/BoxRuntime.java +++ b/src/main/java/ortus/boxlang/runtime/BoxRuntime.java @@ -53,6 +53,7 @@ import ortus.boxlang.runtime.context.RequestBoxContext; import ortus.boxlang.runtime.context.RuntimeBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.dynamic.casters.CastAttempt; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.events.BoxEvent; @@ -224,7 +225,11 @@ public class BoxRuntime implements java.io.Closeable { private ModuleService moduleService; /** - * The JavaBoxPiler instance + * The BoxPiler implementation the runtime will use. At this time we offer two choices: + * 1. JavaBoxpiler - Generates Java source code and compiles it via the JDK + * 2. ASMBoxpiler - Generates bytecode directly via ASM + * However, developers can create their own Boxpiler implementations and register them with the runtime + * via configuration. */ private IBoxpiler boxpiler; @@ -345,17 +350,18 @@ private void loadConfiguration( Boolean debugMode, String configPath ) { this.logger.info( "+ DebugMode detected in config, overriding to {}", this.debugMode ); } - // If in debug mode load the AST Capture listener for debugging - if ( this.debugMode ) { - this.interceptorService.register( - DynamicObject.of( new ASTCapture( false, true ) ), - Key.onParse - ); - } + // AST Capture experimental feature + BooleanCaster.attempt( + this.configuration.experimental.getOrDefault( "ASTCapture", false ) + ).ifSuccessful( + astCapture -> this.interceptorService.register( + DynamicObject.of( new ASTCapture( false, true ) ), + Key.onParse + ) + ); // Load core logger and other core interceptions this.interceptorService.register( new Logging( this ) ); - } /** @@ -445,17 +451,19 @@ private void startup() { // Load the configurations and overrides loadConfiguration( this.debugMode, this.configPath ); + // Anythying below might use configuration items // Ensure home assets ensureHomeAssets(); // Load the Dynamic Class Loader for the runtime - this.runtimeLoader = new DynamicClassLoader( + this.runtimeLoader = new DynamicClassLoader( Key.runtime, getConfiguration().getJavaLibraryPaths(), this.getClass().getClassLoader() ); - + // Startup the right Compiler + this.boxpiler = chooseBoxpiler(); // Seed Mathematical Precision for the runtime MathUtil.setHighPrecisionMath( getConfiguration().useHighPrecisionMath ); @@ -467,9 +475,7 @@ private void startup() { this.applicationService.onStartup(); // Create our runtime context that will be the granddaddy of all contexts that execute inside this runtime - this.runtimeContext = new RuntimeBoxContext(); - this.boxpiler = chooseBoxpiler(); - + this.runtimeContext = new RuntimeBoxContext(); // Now startup the modules so we can have a runtime context available to them this.moduleService.onStartup(); // Now the cache service can be started, this allows for modules to register caches @@ -508,16 +514,6 @@ private void startup() { * The entry point into the runtime */ - private IBoxpiler chooseBoxpiler() { - switch ( ( String ) this.configuration.experimental.getOrDefault( "compiler", "java" ) ) { - case "asm" : - return ASMBoxpiler.getInstance(); - case "java" : - default : - return JavaBoxpiler.getInstance(); - } - } - /** * Get or startup a BoxLang Runtime instance. *

@@ -983,7 +979,6 @@ public Key[] getGlobalServiceKeys() { /** * Switch the runtime to generate java source and compile via the JDK - * */ public void useJavaBoxpiler() { RunnableLoader.getInstance().selectBoxPiler( JavaBoxpiler.class ); @@ -991,7 +986,6 @@ public void useJavaBoxpiler() { /** * Switch the runtime to generate bytecode directly via ASM - * */ public void useASMBoxPiler() { RunnableLoader.getInstance().selectBoxPiler( ASMBoxpiler.class ); @@ -1552,4 +1546,21 @@ private IBoxContext ensureRequestTypeContext( IBoxContext context, URI template } } + /** + * Choose the Boxpiler implementation to use according to the configuration + * + * @return The Boxpiler implementation to use + */ + private IBoxpiler chooseBoxpiler() { + switch ( ( String ) this.configuration.experimental.getOrDefault( "compiler", "java" ) ) { + case "asm" : + useASMBoxPiler(); + return ASMBoxpiler.getInstance(); + case "java" : + default : + useJavaBoxpiler(); + return JavaBoxpiler.getInstance(); + } + } + } diff --git a/src/main/java/ortus/boxlang/runtime/application/Application.java b/src/main/java/ortus/boxlang/runtime/application/Application.java index e09d90601..4173de237 100644 --- a/src/main/java/ortus/boxlang/runtime/application/Application.java +++ b/src/main/java/ortus/boxlang/runtime/application/Application.java @@ -37,7 +37,6 @@ import ortus.boxlang.runtime.context.RequestBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; -import ortus.boxlang.runtime.dynamic.casters.CastAttempt; import ortus.boxlang.runtime.dynamic.casters.LongCaster; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.events.BoxEvent; @@ -121,17 +120,14 @@ public class Application { // Key.TOD, 2147483647 is the largest integer allowed by Java but the ConcurrentStore will allocate 2147483647/4 as the initial size of the Concurent // map and will result in OOM errors Key.maxObjects, 100000, - // Minutes Key.defaultLastAccessTimeout, 3600, - // Minutes Key.defaultTimeout, 3600, Key.objectStore, "ConcurrentStore", - // Seconds Key.reapFrequency, 120, Key.resetTimeoutOnAccess, true, - Key.useLastAccessTimeouts, true + Key.useLastAccessTimeouts, false ); - private static final Key DEFAULT_SESSION_CACHEKEY = Key.boxlangSessions; + private static final Key DEFAULT_SESSION_CACHEKEY = Key.bxSessions; /** * An application can have a collection of class loaders that it can track and manage. @@ -248,7 +244,9 @@ public BaseApplicationListener getStartingListener() { /** * Start the application if not already started * - * @param context The context + * @param context The context starting up the application + * + * @return The started application */ public Application start( IBoxContext context ) { // Apps started, just return @@ -301,31 +299,30 @@ public Application start( IBoxContext context ) { * @throws BoxRuntimeException If the session storage cache is not a string */ private void startupSessionStorage( ApplicationBoxContext appContext ) { - IStruct settings = this.startingListener.getSettings(); - CastAttempt directiveAttempt = StringCaster.attempt( settings.get( Key.sessionStorage ) ); - String sessionStorage = SESSION_STORAGE_MEMORY; - - // Let's be nice and tell them what they put is not good if not a string. - if ( directiveAttempt.ifFailed() ) { - throw new BoxRuntimeException( "Session storage directive must be a string that matches a registered cache" ); - } else { - sessionStorage = directiveAttempt.get().trim(); - } - - // If empty, default it - if ( sessionStorage.isEmpty() ) { - sessionStorage = SESSION_STORAGE_MEMORY; - } - - // Get the cache name according to the storage directive or default to memory - Key sessionCacheName = sessionStorage.equals( SESSION_STORAGE_MEMORY ) + // @formatter:off + IStruct settings = this.startingListener.getSettings(); + String sessionStorageName = StringCaster + .attempt( settings.get( Key.sessionStorage ) ) + // If not a string, advice the user + .ifEmpty( () -> { + throw new BoxRuntimeException( "Session storage directive must be a string that matches a registered cache" ); + } ) + // If present, make sure it has a value or default it + .map( ( String setting ) -> setting.trim().isEmpty() ? SESSION_STORAGE_MEMORY : setting.trim() ) + // Return the right value or the default name + .getOrDefault( SESSION_STORAGE_MEMORY ); + // @formatter:on + + // Now we can get the right cache name to use + Key sessionCacheName = sessionStorageName.equals( SESSION_STORAGE_MEMORY ) ? DEFAULT_SESSION_CACHEKEY - : Key.of( sessionStorage ); + : Key.of( sessionStorageName ); // Create the memory cache if not already created + // This is a stop-gap, it should really never run. But if it does log it so we can fix it if ( sessionCacheName.equals( DEFAULT_SESSION_CACHEKEY ) && !cacheService.hasCache( DEFAULT_SESSION_CACHEKEY ) ) { - logger.debug( "Creating default session memory cache as it doesn't exist" ); + logger.warn( "Creating default session memory cache as it doesn't exist" ); cacheService.createCache( DEFAULT_SESSION_CACHEKEY, diff --git a/src/main/java/ortus/boxlang/runtime/application/ApplicationClassListener.java b/src/main/java/ortus/boxlang/runtime/application/ApplicationClassListener.java index 474e487df..4a12f411f 100644 --- a/src/main/java/ortus/boxlang/runtime/application/ApplicationClassListener.java +++ b/src/main/java/ortus/boxlang/runtime/application/ApplicationClassListener.java @@ -81,7 +81,7 @@ public IClassRunnable getListenerClass() { public void onRequest( IBoxContext context, Object[] args ) { super.onRequest( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onRequest ) ) { + if ( listener.getThisScope().containsKey( Key.onRequest ) ) { listener.dereferenceAndInvoke( context, Key.onRequest, args, false ); } else { // Default includes template inside the Class's context @@ -105,7 +105,7 @@ public void onRequest( IBoxContext context, Object[] args ) { public boolean onRequestStart( IBoxContext context, Object[] args ) { super.onRequestStart( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onRequestStart ) ) { + if ( listener.getThisScope().containsKey( Key.onRequestStart ) ) { Object result = listener.dereferenceAndInvoke( context, Key.onRequestStart, args, false ); if ( result != null ) { return BooleanCaster.cast( result ); @@ -121,7 +121,7 @@ public boolean onRequestStart( IBoxContext context, Object[] args ) { public void onSessionStart( IBoxContext context, Object[] args ) { super.onSessionStart( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onSessionStart ) ) { + if ( listener.getThisScope().containsKey( Key.onSessionStart ) ) { listener.dereferenceAndInvoke( context, Key.onSessionStart, args, false ); } } @@ -130,7 +130,7 @@ public void onSessionStart( IBoxContext context, Object[] args ) { public void onApplicationStart( IBoxContext context, Object[] args ) { super.onApplicationStart( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onApplicationStart ) ) { + if ( listener.getThisScope().containsKey( Key.onApplicationStart ) ) { listener.dereferenceAndInvoke( context, Key.onApplicationStart, args, false ); } } @@ -139,7 +139,7 @@ public void onApplicationStart( IBoxContext context, Object[] args ) { public void onRequestEnd( IBoxContext context, Object[] args ) { super.onRequestEnd( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onRequestEnd ) ) { + if ( listener.getThisScope().containsKey( Key.onRequestEnd ) ) { listener.dereferenceAndInvoke( context, Key.onRequestEnd, args, false ); } } @@ -148,7 +148,7 @@ public void onRequestEnd( IBoxContext context, Object[] args ) { public void onAbort( IBoxContext context, Object[] args ) { super.onAbort( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onAbort ) ) { + if ( listener.getThisScope().containsKey( Key.onAbort ) ) { listener.dereferenceAndInvoke( context, Key.onAbort, args, false ); } } @@ -157,7 +157,7 @@ public void onAbort( IBoxContext context, Object[] args ) { public void onSessionEnd( IBoxContext context, Object[] args ) { super.onSessionEnd( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onSessionEnd ) ) { + if ( listener.getThisScope().containsKey( Key.onSessionEnd ) ) { listener.dereferenceAndInvoke( context, Key.onSessionEnd, args, false ); } } @@ -166,7 +166,7 @@ public void onSessionEnd( IBoxContext context, Object[] args ) { public void onApplicationEnd( IBoxContext context, Object[] args ) { super.onApplicationEnd( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onApplicationEnd ) ) { + if ( listener.getThisScope().containsKey( Key.onApplicationEnd ) ) { listener.dereferenceAndInvoke( context, Key.onApplicationEnd, args, false ); } } @@ -175,7 +175,7 @@ public void onApplicationEnd( IBoxContext context, Object[] args ) { public boolean onError( IBoxContext context, Object[] args ) { super.onError( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onError ) ) { + if ( listener.getThisScope().containsKey( Key.onError ) ) { listener.dereferenceAndInvoke( context, Key.onError, args, false ); return true; } else { @@ -187,7 +187,7 @@ public boolean onError( IBoxContext context, Object[] args ) { public boolean onMissingTemplate( IBoxContext context, Object[] args ) { super.onMissingTemplate( context, args ); - if ( listener.getVariablesScope().containsKey( Key.onMissingTemplate ) ) { + if ( listener.getThisScope().containsKey( Key.onMissingTemplate ) ) { Object result = listener.dereferenceAndInvoke( context, Key.onMissingTemplate, args, false ); if ( result != null ) { return BooleanCaster.cast( result ); @@ -220,7 +220,7 @@ public void onClassRequest( IBoxContext context, Object[] args ) { * Any returnFormat annotation on any other method called during the request is ignored, so long as it wasn't the method specified in the URL * Lucee sets the response content type based on the return format, but Adobe doesn't appear to set any response headers at all based on the return format */ - if ( listener.getVariablesScope().get( Key.onClassRequest ) instanceof Function ) { + if ( listener.getThisScope().get( Key.onClassRequest ) instanceof Function ) { invokeClassRequest( context, listener, Key.onClassRequest.getName(), null, args, returnFormat, false ); } else { invokeClassRequest( diff --git a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java index a4a43dfd7..b038ef928 100644 --- a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java +++ b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java @@ -143,8 +143,8 @@ public abstract class BaseApplicationListener { // Stil Considering if they will be core or a module "secureJson", false, "secureJsonPrefix", "", - "allowedFileOperationExtensions", BoxRuntime.getInstance().getConfiguration().allowedFileOperationExtensions, - "disallowedFileOperationExtensions", BoxRuntime.getInstance().getConfiguration().disallowedFileOperationExtensions + "allowedFileOperationExtensions", BoxRuntime.getInstance().getConfiguration().security.allowedFileOperationExtensions, + "disallowedFileOperationExtensions", BoxRuntime.getInstance().getConfiguration().security.disallowedFileOperationExtensions ); /** diff --git a/src/main/java/ortus/boxlang/runtime/application/Session.java b/src/main/java/ortus/boxlang/runtime/application/Session.java index 880346835..f9863afc9 100644 --- a/src/main/java/ortus/boxlang/runtime/application/Session.java +++ b/src/main/java/ortus/boxlang/runtime/application/Session.java @@ -40,14 +40,9 @@ public class Session implements Serializable { /** * The concatenator for session IDs + * We leverage the `:` as it's a standard in many distributed caches like Redis and Couchbase to denote namespaces */ - public static final String ID_CONCATENATOR = "_"; - - /** - * The URL token format - * MOVE TO COMPAT MODULE - */ - public static final String URL_TOKEN_FORMAT = "CFID=%s"; + public static final String ID_CONCATENATOR = ":"; /** * -------------------------------------------------------------------------- @@ -68,12 +63,12 @@ public class Session implements Serializable { /** * Flag for when session has been started */ - private final AtomicBoolean isNew = new AtomicBoolean( true ); + private final AtomicBoolean isNew = new AtomicBoolean( true ); /** * The application name linked to */ - private Key applicationName = null; + private Key applicationName = null; /** * -------------------------------------------------------------------------- @@ -91,9 +86,7 @@ public Session( Key ID, Application application ) { this.ID = ID; this.applicationName = application.getName(); this.sessionScope = new SessionScope(); - - DateTime timeNow = new DateTime(); - String bxid = this.applicationName + ID_CONCATENATOR + ID; + DateTime timeNow = new DateTime(); // Initialize the session scope this.sessionScope.put( Key.jsessionID, ID.getName() ); @@ -101,11 +94,6 @@ public Session( Key ID, Application application ) { this.sessionScope.put( Key.timeCreated, timeNow ); this.sessionScope.put( Key.lastVisit, timeNow ); - // Move these to the COMPAT module - this.sessionScope.put( Key.urlToken, String.format( URL_TOKEN_FORMAT, bxid ) ); - this.sessionScope.put( Key.cfid, ID.getName() ); - this.sessionScope.put( Key.cftoken, 0 ); - // Announce it's creation BoxRuntime.getInstance() .getInterceptorService() diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/decision/IsEmpty.java b/src/main/java/ortus/boxlang/runtime/bifs/global/decision/IsEmpty.java index b61670a2f..4cb84c9a7 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/decision/IsEmpty.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/decision/IsEmpty.java @@ -51,12 +51,15 @@ public IsEmpty() { } /** - * Determine whether a given value is empty + * Determine whether a given value is empty. We check for emptiness of + * anything that can be casted to: Array, Struct, Query, or String. * * @param context The context in which the BIF is being invoked. * @param arguments Argument scope for the BIF. * - * @argument.value The value to test for emptiness. + * @argument.value The value/object to check for emptiness. + * + * @return True if the value is empty, false otherwise. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { Object object = arguments.get( Key.value ); @@ -82,4 +85,4 @@ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { return false; } -} \ No newline at end of file +} diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileCopy.java b/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileCopy.java index 063d92882..b1f382223 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileCopy.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileCopy.java @@ -25,8 +25,9 @@ import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; -import ortus.boxlang.runtime.util.FileSystemUtil; import ortus.boxlang.runtime.types.exceptions.BoxIOException; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.util.FileSystemUtil; @BoxBIF @@ -59,8 +60,15 @@ public FileCopy() { public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { arguments.put( Key.source, FileSystemUtil.expandPath( context, arguments.getAsString( Key.source ) ).absolutePath().toString() ); arguments.put( Key.destination, FileSystemUtil.expandPath( context, arguments.getAsString( Key.destination ) ).absolutePath().toString() ); + + // Make sure there is no attempt to move a file in to disallowed ( e.g. executable ) type + if ( !runtime.getConfiguration().security.isFileOperationAllowed( arguments.getAsString( Key.destination ) ) ) { + throw new BoxRuntimeException( "The destination path contains an extension disallowed by the runtime security settings." ); + } + Path sourcePath = Path.of( arguments.getAsString( Key.source ) ); Path destinationPath = Path.of( arguments.getAsString( Key.destination ) ); + Path destinationDirectory = destinationPath.getParent(); Boolean createPaths = arguments.getAsBoolean( Key.createPath ); diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileMove.java b/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileMove.java index e02243464..fa46ca8a3 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileMove.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/io/FileMove.java @@ -21,6 +21,7 @@ import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.util.FileSystemUtil; @BoxBIF @@ -51,6 +52,10 @@ public FileMove() { public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { String sourcePath = FileSystemUtil.expandPath( context, arguments.getAsString( Key.source ) ).absolutePath().toString(); String destinationPath = FileSystemUtil.expandPath( context, arguments.getAsString( Key.destination ) ).absolutePath().toString(); + // Make sure there is no attempt to move a file in to disallowed ( e.g. executable ) type + if ( !runtime.getConfiguration().security.isFileOperationAllowed( destinationPath ) ) { + throw new BoxRuntimeException( "The destination path contains an extension disallowed by the runtime security settings." ); + } FileSystemUtil.move( sourcePath, destinationPath ); return null; } diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/jdbc/PreserveSingleQuotes.java b/src/main/java/ortus/boxlang/runtime/bifs/global/jdbc/PreserveSingleQuotes.java index 1e01a3575..5dda49cd9 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/jdbc/PreserveSingleQuotes.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/jdbc/PreserveSingleQuotes.java @@ -46,7 +46,7 @@ public PreserveSingleQuotes() { public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { // This BIF itself doesn't do anything. It's largely a placeholder in the AST for the QueryPreserveSingleQuotesVisitor to look for. // The Visitor actually does the work of escaping or preserving single quotes. - System.out.println( "PreserveSingleQuotes._invoke " + arguments.get( Key.variable ) ); + // System.out.println( "PreserveSingleQuotes._invoke " + arguments.get( Key.variable ) ); return arguments.get( Key.variable ); } diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/list/ListFind.java b/src/main/java/ortus/boxlang/runtime/bifs/global/list/ListFind.java index b04b14689..14d30a9f4 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/list/ListFind.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/list/ListFind.java @@ -17,6 +17,7 @@ */ package ortus.boxlang.runtime.bifs.global.list; +import ortus.boxlang.runtime.bifs.BIF; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.bifs.BoxMember; import ortus.boxlang.runtime.bifs.global.array.ArrayFind; @@ -37,6 +38,9 @@ @BoxMember( type = BoxLangType.STRING, name = "listContainsNoCase" ) public class ListFind extends ArrayFind { + private static final Key listContainsKey = new Key( "listContains" ); + private static final Key listContainsNoCaseKey = new Key( "listContainsNoCase" ); + /** * Constructor */ @@ -76,7 +80,8 @@ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { arguments.getAsBoolean( Key.multiCharacterDelimiter ) ) ); - arguments.put( Key.substringMatch, true ); + Key calledName = arguments.getAsKey( BIF.__functionName ); + arguments.put( Key.substringMatch, calledName.equals( listContainsKey ) || calledName.equals( listContainsNoCaseKey ) ? true : false ); return super._invoke( context, arguments ); } diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncode.java b/src/main/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncode.java index 92eaeb231..6eb220125 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncode.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncode.java @@ -29,7 +29,6 @@ import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; -import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; @BoxBIF @@ -41,7 +40,7 @@ public class CharsetEncode extends BIF { public CharsetEncode() { super(); declaredArguments = new Argument[] { - new Argument( true, "any", Key.binary ), + new Argument( true, "byte[]", Key.binary ), new Argument( false, "string", Key.encoding, "utf-8" ) }; } @@ -57,13 +56,10 @@ public CharsetEncode() { * @argument.encoding The charset encoding to use (default: utf-8 ) */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - - if ( ! ( arguments.get( Key.binary ) instanceof byte[] ) ) { - throw new BoxRuntimeException( "The binary value passed to the function [charsetEncode] is not a valid binary object" ); - } + byte[] binary = ( byte[] ) arguments.get( Key.binary ); return StringUtils.toEncodedString( - ( byte[] ) arguments.get( Key.binary ), + binary, Charset.forName( arguments.getAsString( Key.encoding ) ) ); diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/string/ReReplace.java b/src/main/java/ortus/boxlang/runtime/bifs/global/string/ReReplace.java index e147372c9..b596b3ec4 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/string/ReReplace.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/string/ReReplace.java @@ -51,20 +51,20 @@ public ReReplace() { } /** - * + * * Uses a regular expression (regex) to search a string for a string pattern and replace it with another. The search is case-sensitive. - * + * * @param context The context in which the BIF is being invoked. * @param arguments Argument scope for the BIF. - * + * * @argument.string The string to search - * + * * @argument.regex The regular expression to search for - * + * * @argument.substring The string to replace regex with - * + * * @argument.scope The scope to search in (one, all) - * + * */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { String string = arguments.getAsString( Key.string ); @@ -77,6 +77,17 @@ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { regex = "(?i)" + regex; } + // Default string if null + if ( string == null ) { + string = ""; + } + if ( substring == null ) { + substring = ""; + } + if ( regex == null ) { + regex = ""; + } + // Posix replacement for character classes regex = RegexUtil.posixReplace( regex, noCase ); // Ignore non-quantifier curly braces like PERL diff --git a/src/main/java/ortus/boxlang/runtime/config/Configuration.java b/src/main/java/ortus/boxlang/runtime/config/Configuration.java index b76812bc6..a8b755a5f 100644 --- a/src/main/java/ortus/boxlang/runtime/config/Configuration.java +++ b/src/main/java/ortus/boxlang/runtime/config/Configuration.java @@ -33,6 +33,7 @@ import java.util.Set; import java.util.TimeZone; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,10 +45,10 @@ import ortus.boxlang.runtime.config.segments.ModuleConfig; import ortus.boxlang.runtime.config.segments.SecurityConfig; import ortus.boxlang.runtime.config.util.PlaceholderHelper; -import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.dynamic.casters.KeyCaster; import ortus.boxlang.runtime.dynamic.casters.StringCaster; +import ortus.boxlang.runtime.dynamic.casters.StructCaster; import ortus.boxlang.runtime.loader.DynamicClassLoader; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Array; @@ -56,14 +57,14 @@ import ortus.boxlang.runtime.types.exceptions.BoxIOException; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.types.util.DateTimeHelper; -import ortus.boxlang.runtime.types.util.ListUtil; import ortus.boxlang.runtime.util.DataNavigator; import ortus.boxlang.runtime.util.DataNavigator.Navigator; import ortus.boxlang.runtime.util.LocalizationUtil; /** * The BoxLang configuration object representing the core configuration. - * This object is responsible for processing the configuration struct and returning a new configuration object based on the overrides. + * This object is responsible for processing the configuration struct and + * returning a new configuration object based on the overrides. * Each segment is processed individually from the initial configuration struct. * The configuration object can be converted to a struct for serialization. * @@ -83,179 +84,178 @@ public class Configuration implements IConfigSegment { * The directory where the generated classes will be placed * The default is the system temp directory + {@code /boxlang} */ - public String classGenerationDirectory = System.getProperty( "java.io.tmpdir" ) + "boxlang"; + public String classGenerationDirectory = System.getProperty( "java.io.tmpdir" ) + "boxlang"; /** * The debug mode flag which turns on all kinds of debugging information * {@code false} by default */ - public Boolean debugMode = false; + public Boolean debugMode = false; /** * The Timezone to use for the runtime; * Uses the Java Timezone format: {@code America/New_York} * Uses the default system timezone if not set */ - public ZoneId timezone = TimeZone.getDefault().toZoneId(); + public ZoneId timezone = TimeZone.getDefault().toZoneId(); /** * The default locale to use for the runtime * Uses the default system locale if not set */ - public Locale locale = Locale.getDefault(); + public Locale locale = Locale.getDefault(); /** * Invoke implicit getters and setters when using the implicit accessor * {@code true} by default */ - public Boolean invokeImplicitAccessor = true; + public Boolean invokeImplicitAccessor = true; /** - * Use high precision math for all math operations, else it relies on Double precision + * Use high precision math for all math operations, else it relies on Double + * precision * {@code true} by default */ - public Boolean useHighPrecisionMath = true; + public Boolean useHighPrecisionMath = true; /** * The application timeout * {@code 0} means no timeout and is the default */ - public Duration applicationTimeout = Duration.ofDays( 0 ); + public Duration applicationTimeout = Duration.ofDays( 0 ); /** * The request timeout * {@code 0} means no timeout and is the default */ - public Duration requestTimeout = Duration.ofSeconds( 0 );; + public Duration requestTimeout = Duration.ofSeconds( 0 );; /** * The session timeout * {@code 30} minutes by default */ - public Duration sessionTimeout = Duration.ofMinutes( 30 ); + public Duration sessionTimeout = Duration.ofMinutes( 30 ); /** - * This flag enables/disables session management in the runtime for all applications by default. + * This flag enables/disables session management in the runtime for all + * applications by default. * {@code false} by default */ - public Boolean sessionManagement = false; + public Boolean sessionManagement = false; /** - * The default session storage cache. This has to be the name of a registered cache + * The default session storage cache. This has to be the name of a registered + * cache * or the keyword "memory" which indicates our internal cache. * {@code memory} is the default */ - public String sessionStorage = "memory"; + public String sessionStorage = "memory"; /** - * This determines whether to send CFID and CFTOKEN cookies to the client browser. + * This determines whether to send jSessionID cookies to the client browser. * {@code true} by default */ - public Boolean setClientCookies = true; + public Boolean setClientCookies = true; /** - * Sets CFID and CFTOKEN cookies for a domain (not a host) Required, for applications running on clusters + * Sets jSessionID cookies for a domain (not a host) Required, for applications + * running on clusters * {@code true} by default */ - public Boolean setDomainCookies = true; + public Boolean setDomainCookies = true; /** * A sorted struct of mappings */ - public IStruct mappings = new Struct( Struct.KEY_LENGTH_LONGEST_FIRST_COMPARATOR ); + public IStruct mappings = new Struct( Struct.KEY_LENGTH_LONGEST_FIRST_COMPARATOR ); /** * An array of directories where modules are located and loaded from. * {@code [ /{boxlang-home}/modules ]} */ - public List modulesDirectory = new ArrayList<>( + public List modulesDirectory = new ArrayList<>( Arrays.asList( BoxRuntime.getInstance().getRuntimeHome().toString() + "/modules" ) ); /** * The default logs directory for the runtime */ - public String logsDirectory = Paths.get( BoxRuntime.getInstance().getRuntimeHome().toString(), "/logs" ).normalize() + public String logsDirectory = Paths.get( BoxRuntime.getInstance().getRuntimeHome().toString(), "/logs" ).normalize() .toString(); /** * An array of directories where custom tags are located and loaded from. * {@code [ /{boxlang-home}/customTags ]} */ - public List customTagsDirectory = new ArrayList<>( + public List customTagsDirectory = new ArrayList<>( Arrays.asList( BoxRuntime.getInstance().getRuntimeHome().toString() + "/customTags" ) ); /** * An array of directories where jar files will be loaded from at runtime. */ - public List javaLibraryPaths = new ArrayList<>( + public List javaLibraryPaths = new ArrayList<>( Arrays.asList( BoxRuntime.getInstance().getRuntimeHome().toString() + "/lib" ) ); /** * Cache registrations */ - public IStruct caches = new Struct(); + public IStruct caches = new Struct(); /** * Default datasource registration */ - public String defaultDatasource = ""; + public String defaultDatasource = ""; /** * Global datasource registrations */ - public IStruct datasources = new Struct(); + public IStruct datasources = new Struct(); /** - * Default remote class method return format when executing a method from web runtimes. + * Default remote class method return format when executing a method from web + * runtimes. * The default is JSON */ - public String defaultRemoteMethodReturnFormat = "json"; + public String defaultRemoteMethodReturnFormat = "json"; /** * Default cache registration */ - public CacheConfig defaultCache = new CacheConfig(); + public CacheConfig defaultCache = new CacheConfig(); /** * The modules configuration */ - public IStruct modules = new Struct(); + public IStruct modules = new Struct(); /** * The last config struct loaded */ - public IStruct originalConfig = new Struct(); + public IStruct originalConfig = new Struct(); /** * A collection of all the registered global executors */ - public IStruct executors = new Struct(); - - /** - * File extensions which are disallowed for file operations. The allowed array overrides any items in the disallow list. - */ - public List allowedFileOperationExtensions = new ArrayList<>(); - public List disallowedFileOperationExtensions = new ArrayList<>(); + public IStruct executors = new Struct(); /** * Valid BoxLang class extensions */ - public Set validClassExtensions = new HashSet<>(); + public Set validClassExtensions = new HashSet<>(); /** * Valid BoxLang template extensions */ - public Set validTemplateExtensions = new HashSet<>(); + public Set validTemplateExtensions = new HashSet<>(); /** * Experimental Features */ - public IStruct experimental = new Struct(); + public IStruct experimental = new Struct(); /** * The security configuration */ - public SecurityConfig security = new SecurityConfig(); + public SecurityConfig security = new SecurityConfig(); /** * -------------------------------------------------------------------------- @@ -266,7 +266,7 @@ public class Configuration implements IConfigSegment { /** * Logger */ - private static final Logger logger = LoggerFactory.getLogger( Configuration.class ); + private static final Logger logger = LoggerFactory.getLogger( Configuration.class ); /** * -------------------------------------------------------------------------- @@ -275,9 +275,11 @@ public class Configuration implements IConfigSegment { */ /** - * Processes a configuration struct and returns a new configuration object based on the overrides. + * Processes a configuration struct and returns a new configuration object based + * on the overrides. * - * This method makes sure all elements in the incoming configuration struct are processed and applied to the configuration object. + * This method makes sure all elements in the incoming configuration struct are + * processed and applied to the configuration object. * * @param config the configuration struct * @@ -287,9 +289,12 @@ public Configuration process( IStruct config ) { // Store original config this.originalConfig = config; - // Debug Mode + // Debug Mode || Debbuging Enabled (cfconfig) if ( config.containsKey( "debugMode" ) ) { - this.debugMode = ( Boolean ) config.get( "debugMode" ); + this.debugMode = BooleanCaster.cast( PlaceholderHelper.resolve( config.get( "debugMode" ) ) ); + } + if ( config.containsKey( "debuggingEnabled" ) ) { + this.debugMode = BooleanCaster.cast( PlaceholderHelper.resolve( config.get( "debuggingEnabled" ) ) ); } // Compiler @@ -324,18 +329,22 @@ public Configuration process( IStruct config ) { } // Application Timeout - if ( config.containsKey( Key.applicationTimeout ) && StringCaster.cast( config.get( "applicationTimeout" ) ).length() > 0 ) { - this.applicationTimeout = DateTimeHelper.timespanToDuration( PlaceholderHelper.resolve( config.get( "applicationTimeout" ) ) ); + if ( config.containsKey( Key.applicationTimeout ) + && StringCaster.cast( config.get( "applicationTimeout" ) ).length() > 0 ) { + this.applicationTimeout = DateTimeHelper + .timespanToDuration( PlaceholderHelper.resolve( config.get( "applicationTimeout" ) ) ); } // Request Timeout if ( config.containsKey( Key.requestTimeout ) && StringCaster.cast( config.get( "requestTimeout" ) ).length() > 0 ) { - this.requestTimeout = DateTimeHelper.timespanToDuration( PlaceholderHelper.resolve( config.get( "requestTimeout" ) ) ); + this.requestTimeout = DateTimeHelper + .timespanToDuration( PlaceholderHelper.resolve( config.get( "requestTimeout" ) ) ); } // Session Timeout if ( config.containsKey( Key.sessionTimeout ) && StringCaster.cast( config.get( "sessionTimeout" ) ).length() > 0 ) { - this.sessionTimeout = DateTimeHelper.timespanToDuration( PlaceholderHelper.resolve( config.get( "sessionTimeout" ) ) ); + this.sessionTimeout = DateTimeHelper + .timespanToDuration( PlaceholderHelper.resolve( config.get( "sessionTimeout" ) ) ); } // Session Management @@ -366,8 +375,7 @@ public Configuration process( IStruct config ) { if ( config.get( Key.mappings ) instanceof Map castedMap ) { castedMap.forEach( ( key, value ) -> this.mappings.put( Key.of( key ), - PlaceholderHelper.resolve( value ) - ) ); + PlaceholderHelper.resolve( value ) ) ); } else { logger.warn( "The [runtime.mappings] configuration is not a JSON Object, ignoring it." ); } @@ -421,7 +429,8 @@ public Configuration process( IStruct config ) { // Process the default method return format if ( config.containsKey( Key.defaultRemoteMethodReturnFormat ) ) { - this.defaultRemoteMethodReturnFormat = PlaceholderHelper.resolve( config.get( Key.defaultRemoteMethodReturnFormat ) ).toLowerCase(); + this.defaultRemoteMethodReturnFormat = PlaceholderHelper + .resolve( config.get( Key.defaultRemoteMethodReturnFormat ) ).toLowerCase(); } // Process default cache configuration @@ -440,11 +449,18 @@ public Configuration process( IStruct config ) { castedCaches .entrySet() .forEach( entry -> { + + // We ignore `default` caches, not accepted in boxlang. + if ( StringUtils.equalsIgnoreCase( ( String ) entry.getKey(), "default" ) ) { + return; + } + if ( entry.getValue() instanceof Map castedMap ) { CacheConfig cacheConfig = new CacheConfig( ( String ) entry.getKey() ).process( new Struct( castedMap ) ); this.caches.put( cacheConfig.name, cacheConfig ); } else { - logger.warn( "The [caches.{}] configuration is not a JSON Object, ignoring it.", entry.getKey() ); + logger.warn( "The [caches.{}] configuration is not a JSON Object, ignoring it.", + entry.getKey() ); } } ); } else { @@ -460,10 +476,12 @@ public Configuration process( IStruct config ) { .entrySet() .forEach( entry -> { if ( entry.getValue() instanceof Map castedMap ) { - ExecutorConfig executorConfig = new ExecutorConfig( ( String ) entry.getKey() ).process( new Struct( castedMap ) ); + ExecutorConfig executorConfig = new ExecutorConfig( ( String ) entry.getKey() ) + .process( new Struct( castedMap ) ); this.executors.put( executorConfig.name, executorConfig ); } else { - logger.warn( "The [executors.{}] configuration is not a JSON Object, ignoring it.", entry.getKey() ); + logger.warn( "The [executors.{}] configuration is not a JSON Object, ignoring it.", + entry.getKey() ); } } ); } else { @@ -475,7 +493,8 @@ public Configuration process( IStruct config ) { if ( config.containsKey( Key.validClassExtensions ) ) { if ( config.get( Key.validClassExtensions ) instanceof List castedList ) { // iterate and add to the original list if it doesn't exist - castedList.forEach( item -> this.validClassExtensions.add( PlaceholderHelper.resolve( item ).toLowerCase() ) ); + castedList + .forEach( item -> this.validClassExtensions.add( PlaceholderHelper.resolve( item ).toLowerCase() ) ); } else { logger.warn( "The [validClassExtensions] configuration is not a JSON Array, ignoring it." ); } @@ -485,7 +504,8 @@ public Configuration process( IStruct config ) { if ( config.containsKey( Key.validTemplateExtensions ) ) { if ( config.get( Key.validTemplateExtensions ) instanceof List castedList ) { // iterate and add to the original list if it doesn't exist - castedList.forEach( item -> this.validTemplateExtensions.add( PlaceholderHelper.resolve( item ).toLowerCase() ) ); + castedList.forEach( + item -> this.validTemplateExtensions.add( PlaceholderHelper.resolve( item ).toLowerCase() ) ); } else { logger.warn( "The [validTemplateExtensions] configuration is not a JSON Array, ignoring it." ); } @@ -513,10 +533,13 @@ public Configuration process( IStruct config ) { .entrySet() .forEach( entry -> { if ( entry.getValue() instanceof Map castedMap ) { - DatasourceConfig datasourceConfig = new DatasourceConfig( Key.of( entry.getKey() ) ).process( new Struct( castedMap ) ); + DatasourceConfig datasourceConfig = new DatasourceConfig( Key.of( entry.getKey() ) ) + .process( new Struct( castedMap ) ); this.datasources.put( datasourceConfig.name, datasourceConfig ); } else { - logger.warn( "The [runtime.datasources.{}] configuration is not a JSON Object, ignoring it.", entry.getKey() ); + logger.warn( + "The [runtime.datasources.{}] configuration is not a JSON Object, ignoring it.", + entry.getKey() ); } } ); } else { @@ -532,10 +555,12 @@ public Configuration process( IStruct config ) { .entrySet() .forEach( entry -> { if ( entry.getValue() instanceof Map castedMap ) { - ModuleConfig moduleConfig = new ModuleConfig( KeyCaster.cast( entry.getKey() ).getName() ).process( new Struct( castedMap ) ); + ModuleConfig moduleConfig = new ModuleConfig( KeyCaster.cast( entry.getKey() ).getName() ) + .process( new Struct( castedMap ) ); this.modules.put( moduleConfig.name, moduleConfig ); } else { - logger.warn( "The [runtime.modules.{}] configuration is not a JSON Object, ignoring it.", entry.getKey() ); + logger.warn( "The [runtime.modules.{}] configuration is not a JSON Object, ignoring it.", + entry.getKey() ); } } ); @@ -544,26 +569,9 @@ public Configuration process( IStruct config ) { } } - // File operation safety keys - if ( config.containsKey( Key.allowedFileOperationExtensions ) ) { - if ( config.get( Key.allowedFileOperationExtensions ) instanceof String ) { - config.put( Key.allowedFileOperationExtensions, - ListUtil.asList( config.getAsString( Key.allowedFileOperationExtensions ), ListUtil.DEFAULT_DELIMITER ) ); - } - - // For some reason we have to re-cast this through a stream. Attempting to cast it directly throws a ClassCastException - this.allowedFileOperationExtensions = ArrayCaster.cast( config.get( Key.allowedFileOperationExtensions ) ).stream().map( StringCaster::cast ) - .toList(); - } - - if ( config.containsKey( Key.disallowedFileOperationExtensions ) ) { - if ( config.get( Key.disallowedFileOperationExtensions ) instanceof String ) { - config.put( Key.disallowedFileOperationExtensions, - ListUtil.asList( config.getAsString( Key.disallowedFileOperationExtensions ), ListUtil.DEFAULT_DELIMITER ) ); - } - // For some reason we have to re-cast this through a stream. Attempting to cast it directly throws a ClassCastException - this.disallowedFileOperationExtensions = ArrayCaster.cast( config.get( Key.disallowedFileOperationExtensions ) ).stream().map( StringCaster::cast ) - .toList(); + // Process our security configuration + if ( config.containsKey( Key.security ) ) { + security.process( StructCaster.cast( config.get( Key.security ) ) ); } return this; @@ -589,7 +597,8 @@ public String[] getRegisteredMappings() { /** * Verify if a mapping exists * - * @param mapping The mapping to verify: {@code /myMapping}, please note the leading slash + * @param mapping The mapping to verify: {@code /myMapping}, please note the + * leading slash * * @return True if the mapping exists, false otherwise */ @@ -600,7 +609,8 @@ public boolean hasMapping( String mapping ) { /** * Verify if a mapping exists * - * @param mapping The mapping to verify: {@code /myMapping}, please note the leading slash + * @param mapping The mapping to verify: {@code /myMapping}, please note the + * leading slash * * @return True if the mapping exists, false otherwise */ @@ -615,7 +625,8 @@ public boolean hasMapping( Key mapping ) { /** * Register a mapping in the runtime configuration * - * @param mapping The mapping to register: {@code /myMapping}, please note the leading slash + * @param mapping The mapping to register: {@code /myMapping}, please note the + * leading slash * @param path The absolute path to the directory to map to the mapping * * @throws BoxRuntimeException If the path does not exist @@ -629,7 +640,8 @@ public Configuration registerMapping( String mapping, String path ) { /** * Register a mapping in the runtime configuration * - * @param mapping The mapping to register: {@code /myMapping}, please note the leading slash + * @param mapping The mapping to register: {@code /myMapping}, please note the + * leading slash * @param path The absolute path to the directory to map to the mapping * * @throws BoxRuntimeException If the path does not exist @@ -648,8 +660,7 @@ public Configuration registerMapping( Key mapping, String path ) { // Verify it exists else throw an exception if ( !pathObj.toFile().exists() ) { throw new BoxRuntimeException( - String.format( "The path [%s] does not exist.", pathObj ) - ); + String.format( "The path [%s] does not exist.", pathObj ) ); } // Now we can add it @@ -661,7 +672,8 @@ public Configuration registerMapping( Key mapping, String path ) { /** * Unregister a mapping in the runtime configuration * - * @param mapping The String mapping to unregister: {@code /myMapping}, please note the leading slash + * @param mapping The String mapping to unregister: {@code /myMapping}, please + * note the leading slash * * @return True if the mapping was removed, false otherwise */ @@ -672,7 +684,8 @@ public boolean unregisterMapping( String mapping ) { /** * Unregister a mapping in the runtime configuration using a {@link Key} * - * @param mapping The Key mapping to unregister: {@code /myMapping}, please note the leading slash + * @param mapping The Key mapping to unregister: {@code /myMapping}, please note + * the leading slash * * @return True if the mapping was removed, false otherwise */ @@ -693,7 +706,8 @@ public boolean unregisterMapping( Key mapping ) { /** * Get the java library paths as an array of URLs of Jar files - * This is usually called by the runtime to load all the JARs in the paths to the runtime classloader + * This is usually called by the runtime to load all the JARs in the paths to + * the runtime classloader * * @throws BoxIOException If a path is not a valid path * @@ -725,13 +739,17 @@ public URL[] getJavaLibraryPaths() { } /** - * Helper method to validate datasource drivers configured in the runtime configuration - * This makes sure all declared drivers are registered with the datasource service + * Helper method to validate datasource drivers configured in the runtime + * configuration + * This makes sure all declared drivers are registered with the datasource + * service * - * @throws BoxRuntimeException If a datasource driver is not registered with the datasource service + * @throws BoxRuntimeException If a datasource driver is not registered with the + * datasource service */ public void validateDatsourceDrivers() { - // iterate over all datasources and validate the drivers exists in the datasource service, else throw an exception + // iterate over all datasources and validate the drivers exists in the + // datasource service, else throw an exception this.datasources.entrySet().forEach( entry -> { DatasourceConfig datasource = ( DatasourceConfig ) entry.getValue(); if ( !BoxRuntime.getInstance().getDataSourceService().hasDriver( datasource.getDriver() ) ) { @@ -739,9 +757,7 @@ public void validateDatsourceDrivers() { String.format( "The datasource [%s] has a driver [%s] that is not registered with the datasource service.", datasource.name, - datasource.getDriver() - ) - ); + datasource.getDriver() ) ); } } ); } @@ -755,7 +771,8 @@ public void validateDatsourceDrivers() { * config.navigate( "originalConfig" ) * * - * @param path The path to the object in the data structure. By default it's the root. + * @param path The path to the object in the data structure. By default it's the + * root. * * @return The navigator with a potential navigation path set */ @@ -792,19 +809,23 @@ public IStruct asStruct() { mappingsCopy.putAll( this.mappings ); IStruct cachesCopy = new Struct(); - this.caches.entrySet().forEach( entry -> cachesCopy.put( entry.getKey(), ( ( CacheConfig ) entry.getValue() ).toStruct() ) ); + this.caches.entrySet() + .forEach( entry -> cachesCopy.put( entry.getKey(), ( ( CacheConfig ) entry.getValue() ).toStruct() ) ); IStruct executorsCopy = new Struct(); - this.executors.entrySet().forEach( entry -> executorsCopy.put( entry.getKey(), ( ( ExecutorConfig ) entry.getValue() ).toStruct() ) ); + this.executors.entrySet() + .forEach( entry -> executorsCopy.put( entry.getKey(), ( ( ExecutorConfig ) entry.getValue() ).toStruct() ) ); IStruct datsourcesCopy = new Struct(); - this.datasources.entrySet().forEach( entry -> datsourcesCopy.put( entry.getKey(), ( ( DatasourceConfig ) entry.getValue() ).asStruct() ) ); + this.datasources.entrySet() + .forEach( entry -> datsourcesCopy.put( entry.getKey(), ( ( DatasourceConfig ) entry.getValue() ).asStruct() ) ); IStruct modulesCopy = new Struct(); - this.modules.entrySet().forEach( entry -> modulesCopy.put( entry.getKey(), ( ( ModuleConfig ) entry.getValue() ).asStruct() ) ); + this.modules.entrySet() + .forEach( entry -> modulesCopy.put( entry.getKey(), ( ( ModuleConfig ) entry.getValue() ).asStruct() ) ); return Struct.of( - Key.allowedFileOperationExtensions, Array.fromList( this.allowedFileOperationExtensions ), + Key.applicationTimeout, this.applicationTimeout, Key.caches, cachesCopy, Key.classGenerationDirectory, this.classGenerationDirectory, @@ -814,7 +835,6 @@ public IStruct asStruct() { Key.defaultCache, this.defaultCache.toStruct(), Key.defaultDatasource, this.defaultDatasource, Key.defaultRemoteMethodReturnFormat, this.defaultRemoteMethodReturnFormat, - Key.disallowedFileOperationExtensions, Array.fromList( this.disallowedFileOperationExtensions ), Key.executors, executorsCopy, Key.experimental, Struct.fromMap( this.experimental ), Key.invokeImplicitAccessor, this.invokeImplicitAccessor, @@ -835,7 +855,6 @@ public IStruct asStruct() { Key.useHighPrecisionMath, this.useHighPrecisionMath, Key.validExtensions, Array.fromSet( getValidExtensions() ), Key.validClassExtensions, Array.fromSet( this.validClassExtensions ), - Key.validTemplateExtensions, Array.fromSet( this.validTemplateExtensions ) - ); + Key.validTemplateExtensions, Array.fromSet( this.validTemplateExtensions ) ); } } diff --git a/src/main/java/ortus/boxlang/runtime/config/segments/DatasourceConfig.java b/src/main/java/ortus/boxlang/runtime/config/segments/DatasourceConfig.java index 2675d8b9b..d5ba91757 100644 --- a/src/main/java/ortus/boxlang/runtime/config/segments/DatasourceConfig.java +++ b/src/main/java/ortus/boxlang/runtime/config/segments/DatasourceConfig.java @@ -325,6 +325,15 @@ public Key getUniqueName() { return Key.of( uniqueName.toString() ); } + /** + * Get the original name of the datasource - this is NOT unique and should not be used for identification. + * + * @return The original name of the datasource. + */ + public String getOriginalName() { + return this.name.getName(); + } + /** * Processes the state of the configuration segment from the configuration struct. *

diff --git a/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java b/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java index 3375ca683..4e054cc2b 100644 --- a/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java +++ b/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java @@ -17,7 +17,9 @@ */ package ortus.boxlang.runtime.config.segments; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -25,8 +27,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.io.Files; + import ortus.boxlang.runtime.config.util.PropertyHelper; import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.Array; import ortus.boxlang.runtime.types.IStruct; import ortus.boxlang.runtime.types.Struct; @@ -40,19 +45,25 @@ public class SecurityConfig implements IConfigSegment { * These are a list of regular expressions that are used to match against the import statements in the code * Ex: "disallowedImports": ["java\\.lang\\.(ProcessBuilder|Reflect", "java\\.io\\.(File|FileWriter)"] */ - public Set disallowedImports = new HashSet<>(); + public Set disallowedImports = new HashSet<>(); /** * Disallowed BIFs in the runtime * Ex: "disallowedBifs": ["createObject", "systemExecute"] */ - public Set disallowedBIFs = new HashSet<>(); + public Set disallowedBIFs = new HashSet<>(); /** * Disallowed Components in the runtime * Ex: "disallowedComponents": [ "execute", "http" ] */ - public Set disallowedComponents = new HashSet<>(); + public Set disallowedComponents = new HashSet<>(); + + /** + * File extensions which are disallowed for file operations. The allowed array overrides any items in the disallow list. + */ + public List allowedFileOperationExtensions = new ArrayList<>(); + public List disallowedFileOperationExtensions = new ArrayList<>(); /** * -------------------------------------------------------------------------- @@ -63,14 +74,14 @@ public class SecurityConfig implements IConfigSegment { /** * Logger */ - private static final Logger logger = LoggerFactory.getLogger( SecurityConfig.class ); + private static final Logger logger = LoggerFactory.getLogger( SecurityConfig.class ); /** * Maps of allowed BIFs so lookups get faster as we go */ - public Map allowedBIFsLookup = new ConcurrentHashMap<>(); - public Map allowedComponentsLookup = new ConcurrentHashMap<>(); - public Map allowedImportsLookup = new ConcurrentHashMap<>(); + public Map allowedBIFsLookup = new ConcurrentHashMap<>(); + public Map allowedComponentsLookup = new ConcurrentHashMap<>(); + public Map allowedImportsLookup = new ConcurrentHashMap<>(); /** * -------------------------------------------------------------------------- @@ -146,6 +157,33 @@ public boolean isClassAllowed( String name ) { return true; } + /** + * Determines whether a file operation is allowed or not based on the file extension. + * + * @param file + * + * @return + */ + public boolean isFileOperationAllowed( String file ) { + String fileExtension = Files.getFileExtension( file ); + return this.isExtensionAllowed( fileExtension ); + } + + /** + * Determines whether a file extension is allowed or not. + * + * @param extension + * + * @return + */ + public boolean isExtensionAllowed( String extension ) { + if ( this.allowedFileOperationExtensions.contains( extension ) ) { + return true; + } else { + return !this.disallowedFileOperationExtensions.contains( extension ); + } + } + /** * Processes the configuration struct. Each segment is processed individually from the initial configuration struct. * @@ -158,6 +196,8 @@ public IConfigSegment process( IStruct config ) { PropertyHelper.processListToSet( config, Key.disallowedImports, this.disallowedImports ); PropertyHelper.processListToSet( config, Key.disallowedBIFs, this.disallowedBIFs ); PropertyHelper.processListToSet( config, Key.disallowedComponents, this.disallowedComponents ); + PropertyHelper.processStringOrArrayToList( config, Key.allowedFileOperationExtensions, this.allowedFileOperationExtensions ); + PropertyHelper.processStringOrArrayToList( config, Key.disallowedFileOperationExtensions, this.disallowedFileOperationExtensions ); return this; } @@ -167,9 +207,11 @@ public IConfigSegment process( IStruct config ) { @Override public IStruct asStruct() { return Struct.of( + Key.allowedFileOperationExtensions, Array.fromList( this.allowedFileOperationExtensions ), Key.disallowedImports, this.disallowedImports, Key.disallowedBIFs, this.disallowedBIFs, - Key.disallowedComponents, this.disallowedComponents + Key.disallowedComponents, this.disallowedComponents, + Key.disallowedFileOperationExtensions, Array.fromList( this.disallowedFileOperationExtensions ) ); } diff --git a/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java b/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java index a7734f467..dd951b218 100644 --- a/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java +++ b/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java @@ -24,8 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; +import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.util.ListUtil; /** * Helps convert from JSON to native Java/BoxLang Types @@ -38,7 +41,7 @@ public class PropertyHelper { private static final Logger logger = LoggerFactory.getLogger( PropertyHelper.class ); /** - * Process the target key + * This processes a JSON array list to a HashSet * * @param config The configuration object * @param key The target key to look and process @@ -55,4 +58,32 @@ public static void processListToSet( IStruct config, Key key, Set target } } + /** + * This processes: + * - A string to a list (comma separated or a single string) + * - A JSON array to a list + * + * @param config The configuration object + * @param key The target key to look and process + * @param target The target list to populate with the values + */ + public static void processStringOrArrayToList( IStruct config, Key key, List target ) { + if ( config.containsKey( key ) ) { + // If it's a string, convert it to an array + if ( config.get( key ) instanceof String castedStringList ) { + config.put( + key, + ListUtil.asList( PlaceholderHelper.resolve( castedStringList ), ListUtil.DEFAULT_DELIMITER ) + ); + } + + // For some reason we have to re-cast this through a stream. Attempting to cast it directly throws a ClassCastException + ArrayCaster.cast( config.get( key ) ) + .stream() + // Add each item to the incoming target + .map( StringCaster::cast ) + .forEach( target::add ); + } + } + } diff --git a/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java index 7f8c706ac..f8275919b 100644 --- a/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java @@ -645,6 +645,9 @@ protected Function findFunction( Key name ) { Object value = result.value(); if ( value instanceof Function fun ) { return fun; + } else if ( value == null ) { + throw new BoxRuntimeException( + "Variable '" + name + "' is null and cannot be used as a function." ); } else { throw new BoxRuntimeException( "Variable '" + name + "' of type '" + value.getClass().getName() + "' is not a function." ); diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/ByteCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/ByteCaster.java index 7ef5a6f39..cc008cce0 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/ByteCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/ByteCaster.java @@ -65,15 +65,23 @@ public static Byte cast( Object object, Boolean fail ) { } } - // TODO: Find a way to check if the string can be cast without throwing an exception here - try { - return Byte.valueOf( StringCaster.cast( object ) ); - } catch ( NumberFormatException e ) { - if ( fail ) { - throw e; - } else { - return null; - } + // Check if it's already a byte + if ( object instanceof Byte b ) { + return b; + } + + // Check if the object is already a number + if ( object instanceof Number num ) { + return num.byteValue(); + } + + CastAttempt number = NumberCaster.attempt( object ); + if ( number.wasSuccessful() ) { + return number.get().byteValue(); + } else if ( fail ) { + throw new BoxCastException( "Can't cast " + object.getClass().getName() + " to a byte." ); + } else { + return null; } } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/FloatCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/FloatCaster.java index 6a4d0b376..9c3543fe7 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/FloatCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/FloatCaster.java @@ -75,23 +75,13 @@ public static Float cast( Object object, Boolean fail ) { return Float.valueOf( bool ? 1 : 0 ); } - // TODO: Find a way to check if the string can be cast without throwing an exception here - try { - String value = StringCaster.cast( object, fail ); - if ( value == null ) { - if ( fail ) { - throw new BoxCastException( "Can't cast null to a float." ); - } else { - return null; - } - } - return Float.valueOf( value ); - } catch ( NumberFormatException e ) { - if ( fail ) { - throw e; - } else { - return null; - } + CastAttempt number = NumberCaster.attempt( object ); + if ( number.wasSuccessful() ) { + return number.get().floatValue(); + } else if ( fail ) { + throw new BoxCastException( "Can't cast " + object.getClass().getName() + " to a float." ); + } else { + return null; } } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java index a6ed5726b..30bcb8e14 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java @@ -17,7 +17,6 @@ */ package ortus.boxlang.runtime.dynamic.casters; -import java.lang.reflect.Array; import java.math.BigDecimal; import java.util.List; import java.util.Optional; @@ -57,9 +56,9 @@ public class GenericCaster implements IBoxCaster { public static CastAttempt attempt( IBoxContext context, Object object, Object oType, boolean strict ) { String type; if ( oType instanceof BoxLangType boxType ) { - type = boxType.name().toLowerCase(); + type = boxType.name(); } else { - type = StringCaster.cast( oType ).toLowerCase(); + type = StringCaster.cast( oType ); } // Represent legit null values in a NullValue instance @@ -138,7 +137,8 @@ public static Object cast( IBoxContext context, Object object, Object oType, Boo String newType = type.substring( 0, type.length() - 2 ); originalCaseType = originalCaseType.substring( 0, originalCaseType.length() - 2 ); Class newTypeClass = getClassFromType( context, newType, originalCaseType, false ); - Object[] result; + // Typed as Object instead of Object[] in case we're creating an array of primitives + Object result; Boolean convertToArray = false; // If we could not get the class, then we are casting to an array of objects @@ -148,9 +148,14 @@ public static Object cast( IBoxContext context, Object object, Object oType, Boo } if ( object.getClass().isArray() ) { + // If our incoming object is already an array of the new type, just return it + if ( object.getClass().getComponentType().equals( newTypeClass ) ) { + return object; + } result = castNativeArrayToNativeArray( context, object, newType, fail, newTypeClass ); - } else if ( object instanceof List ) { - result = castListToNativeArray( context, object, newType, fail, newTypeClass ); + } else if ( object instanceof List incomingList ) { + Object[] incomingArray = incomingList.toArray(); + result = castNativeArrayToNativeArray( context, incomingArray, newType, fail, newTypeClass ); } else { throw new BoxCastException( String.format( "You asked for type %s, but input %s cannot be cast to an array.", type, @@ -158,7 +163,9 @@ public static Object cast( IBoxContext context, Object object, Object oType, Boo ); } if ( convertToArray ) { - return ortus.boxlang.runtime.types.Array.fromArray( result ); + // unsafe cast to Object[] is OK here because the convertToArray flag will never be true + // if our target type is an array of primitives, so we know it will have boxed types in it + return ortus.boxlang.runtime.types.Array.fromArray( ( Object[] ) result ); } return result; } @@ -302,20 +309,23 @@ public static Object cast( IBoxContext context, Object object, Object oType, Boo } - private static Object[] castNativeArrayToNativeArray( IBoxContext context, Object object, String newType, boolean fail, Class newTypeClass ) { - Object[] result = ( Object[] ) java.lang.reflect.Array.newInstance( newTypeClass, Array.getLength( object ) ); - for ( int i = Array.getLength( object ) - 1; i >= 0; i-- ) { - result[ i ] = GenericCaster.cast( context, Array.get( object, i ), newType, fail ); - } - return result; - } - - private static Object[] castListToNativeArray( IBoxContext context, Object object, String newType, boolean fail, Class newTypeClass ) { - List l = ( List ) object; - Object[] incomingList = l.toArray(); - Object[] result = ( Object[] ) java.lang.reflect.Array.newInstance( newTypeClass, incomingList.length ); - for ( int i = incomingList.length - 1; i >= 0; i-- ) { - result[ i ] = GenericCaster.cast( context, incomingList[ i ], newType, fail ); + /** + * Cast a native array to a native array + * We are accepting Object and returning Object so we can pass arrays of primitives + * + * @param object The object to cast + * @param newType The new type + * @param fail True to throw exception when type is invalid + * @param newTypeClass The new type class + * + * @return The casted object + */ + private static Object castNativeArrayToNativeArray( IBoxContext context, Object object, String newType, boolean fail, Class newTypeClass ) { + int len = java.lang.reflect.Array.getLength( object ); + Object result = java.lang.reflect.Array.newInstance( newTypeClass, len ); + for ( int i = len - 1; i >= 0; i-- ) { + Object v = GenericCaster.cast( context, java.lang.reflect.Array.get( object, i ), newType, fail ); + java.lang.reflect.Array.set( result, i, v ); } return result; } @@ -354,6 +364,33 @@ public static Class getClassFromType( IBoxContext context, String type, Strin */ public static Class getClassFromType( IBoxContext context, String type, String originalCaseType, Boolean fail ) { + // Check for primitive types first + if ( originalCaseType.equals( "byte" ) ) { + return byte.class; + } + if ( originalCaseType.equals( "char" ) ) { + return char.class; + } + if ( originalCaseType.equals( "short" ) ) { + return short.class; + } + if ( originalCaseType.equals( "int" ) ) { + return int.class; + } + if ( originalCaseType.equals( "long" ) ) { + return long.class; + } + if ( originalCaseType.equals( "float" ) ) { + return float.class; + } + if ( originalCaseType.equals( "double" ) ) { + return double.class; + } + if ( originalCaseType.equals( "boolean" ) ) { + return boolean.class; + } + + // Check for boxed types if ( type.equals( "bigdecimal" ) || type.equals( "java.math.bigdecimal" ) ) { return BigDecimal.class; } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/LongCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/LongCaster.java index e84d3c30f..d093812e1 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/LongCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/LongCaster.java @@ -75,23 +75,13 @@ public static Long cast( Object object, Boolean fail ) { return Long.valueOf( bool ? 1 : 0 ); } - // TODO: Find a way to check if the string can be cast without throwing an exception here - try { - String value = StringCaster.cast( object, fail ); - if ( value == null ) { - if ( fail ) { - throw new BoxCastException( "Can't cast null to a long." ); - } else { - return null; - } - } - return Long.valueOf( value ); - } catch ( NumberFormatException e ) { - if ( fail ) { - throw e; - } else { - return null; - } + CastAttempt number = NumberCaster.attempt( object ); + if ( number.wasSuccessful() ) { + return number.get().longValue(); + } else if ( fail ) { + throw new BoxCastException( "Can't cast " + object.getClass().getName() + " to a long." ); + } else { + return null; } } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/ShortCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/ShortCaster.java index f5ec5d790..7259669b4 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/ShortCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/ShortCaster.java @@ -75,17 +75,14 @@ public static Short cast( Object object, Boolean fail ) { return Short.valueOf( ( short ) ( bool ? 1 : 0 ) ); } - // TODO: Find a way to check if the string can be cast without throwing an exception here - try { - return Short.valueOf( StringCaster.cast( object ) ); - } catch ( NumberFormatException e ) { - if ( fail ) { - throw e; - } else { - return null; - } + CastAttempt number = NumberCaster.attempt( object ); + if ( number.wasSuccessful() ) { + return number.get().shortValue(); + } else if ( fail ) { + throw new BoxCastException( "Can't cast " + object.getClass().getName() + " to a short." ); + } else { + return null; } - } } diff --git a/src/main/java/ortus/boxlang/runtime/jdbc/ConnectionManager.java b/src/main/java/ortus/boxlang/runtime/jdbc/ConnectionManager.java index d53001366..335ef45e6 100644 --- a/src/main/java/ortus/boxlang/runtime/jdbc/ConnectionManager.java +++ b/src/main/java/ortus/boxlang/runtime/jdbc/ConnectionManager.java @@ -454,7 +454,7 @@ public DataSource getDatasource( Key datasourceName ) { * @return The datasource object */ public DataSource register( DataSource target ) { - this.datasources.put( target.getUniqueName(), target ); + this.datasources.put( target.getConfiguration().name, target ); return target; } diff --git a/src/main/java/ortus/boxlang/runtime/jdbc/DataSource.java b/src/main/java/ortus/boxlang/runtime/jdbc/DataSource.java index 111872a5d..b80866d0d 100644 --- a/src/main/java/ortus/boxlang/runtime/jdbc/DataSource.java +++ b/src/main/java/ortus/boxlang/runtime/jdbc/DataSource.java @@ -113,6 +113,15 @@ public static DataSource fromStruct( Key name, IStruct properties ) { * -------------------------------------------------------------------------- */ + /** + * Get the original name of the datasource - this is NOT unique and should not be used for identification. + * + * @return The original name of the datasource. + */ + public String getOriginalName() { + return this.configuration.getOriginalName(); + } + /** * Get a unique datasource name which includes a hash of the properties * Following the pattern: bx_{name}_{properties_hash} diff --git a/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java b/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java index f1aae2702..4622a4147 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java +++ b/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java @@ -649,6 +649,23 @@ public record ClassLocation( Boolean isFromModule() { return module != null; } + + /** + * Show the state of this record as a string + */ + @Override + public String toString() { + return String.format( + "ClassLocation [name=%s, path=%s, packageName=%s, type=%s, clazz=%s, module=%s, cachable=%s]", + name, + path, + packageName, + type, + clazz, + module, + cachable + ); + } } } diff --git a/src/main/java/ortus/boxlang/runtime/loader/DiskClassLoader.java b/src/main/java/ortus/boxlang/runtime/loader/DiskClassLoader.java index 099357540..7dfcd5c19 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/DiskClassLoader.java +++ b/src/main/java/ortus/boxlang/runtime/loader/DiskClassLoader.java @@ -22,8 +22,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; import ortus.boxlang.compiler.ClassInfo; import ortus.boxlang.compiler.IBoxpiler; @@ -75,33 +76,74 @@ public DiskClassLoader( URL[] urls, ClassLoader parent, Path diskStore, IBoxpile /** * Find class on disk, if not found, delegate to parent + * TODO: If we move to sharing classloaders across files with trusted cache, make the locking more granular instead of synchronizing the entire method * * @param name class name */ @Override - protected Class findClass( String name ) throws ClassNotFoundException { + protected synchronized Class findClass( String name ) throws ClassNotFoundException { Path diskPath = generateDiskPath( name ); // JIT compile - ClassInfo classInfo = boxPiler.getClassPool( classPoolName ).get( IBoxpiler.getBaseFQN( name ) ); - if ( !hasClass( diskPath ) || ( classInfo != null && ( classInfo.lastModified() > diskPath.toFile().lastModified() ) ) ) { - // After this call, the class files will exist on disk - boxPiler.compileClassInfo( classPoolName, name ); + String baseName = IBoxpiler.getBaseFQN( name ); + ClassInfo classInfo = boxPiler.getClassPool( classPoolName ).get( baseName ); + // Do we need to compile the class? + // Pre-compiled source files will follow this path, but will be discovered as already compiled when we try to parse them + if ( needsCompile( classInfo, diskPath, name, baseName ) ) { + // After this call, the class files will exist on disk, or will have been side-loaded into this classloader via the + // defineClass method (for pre-compiled source files) + boxPiler.compileClassInfo( classPoolName, baseName ); } + // If there is no class file on disk, then we assume pre-compiled class bytes were already side loaded in, so we just get them + // TODO: Change this to use the same flag discussed in the needsCompile method if ( !diskPath.toFile().exists() ) { return loadClass( name ); } - // Read file as byte array + + // In all other scenarios, the BoxPiler should have compiled the class and written it to disk byte[] bytes; try { bytes = Files.readAllBytes( diskPath ); } catch ( IOException e ) { throw new ClassNotFoundException( "Unable to read class file from disk", e ); } - return defineClass( name, bytes, 0, bytes.length ); } + /** + * Abstract out the logic for determining if a class needs to be compiled + * + * @param classInfo the class info, may be null if there is none + * @param diskPath the path to the class file on disk, may not exist + * @param name the fully qualified class name + * @param baseName the base name of the class + * + * @return true if the class needs to be compiled + */ + private boolean needsCompile( ClassInfo classInfo, Path diskPath, String name, String baseName ) { + // TODO: need to add some mutable flags to the classInfo object to track if it has been compiled or not + // and in what manner. For example, precompiled sources read directly into the class loader won't + // have a class file on disk and we should be able to just skip that check entirely if we know that. + // Using the existence of the class file on disk is not a good indicator of whether or not the class + // needs to be compiled or not, as a precompiled source file will not have a class file on disk. + + // If we're loading an inner class, we know the compilation has already happened + if ( !name.equals( baseName ) ) { + return false; + } + // There is a class file cached on disk + if ( hasClass( diskPath ) ) { + // If the class file is older than the source file + if ( classInfo != null && classInfo.lastModified() > diskPath.toFile().lastModified() ) { + return true; + } + return false; + } + + // There is no class file cached on disk + return true; + } + /** * Check if a class exists on disk * @@ -175,8 +217,7 @@ public void defineClasses( String fqn, File sourceFile ) { // remove initial magic number buffer.getInt(); - List classBytesList = new ArrayList<>(); - boolean first = true; + boolean first = true; while ( buffer.hasRemaining() ) { // Read the length of the class file @@ -195,18 +236,22 @@ public void defineClasses( String fqn, File sourceFile ) { + "]. Pre-compiled source code must have the same path and name as the original file." ); } } else { + // String className = getClassName( classBytes ); // Define the class defineClass( null, classBytes, 0, classBytes.length ); } } - for ( byte[] classBytes : classBytesList ) { - // Define the class - defineClass( null, classBytes, 0, classBytes.length ); - } } catch ( IOException e ) { throw new RuntimeException( "Failed to read file", e ); } } + public static String getClassName( byte[] classBytes ) { + ClassReader classReader = new ClassReader( classBytes ); + ClassNode classNode = new ClassNode(); + classReader.accept( classNode, 0 ); + return classNode.name.replace( '/', '.' ); + } + } diff --git a/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java b/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java index a3356ee3b..7fe0cf35b 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java +++ b/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java @@ -20,23 +20,36 @@ import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; /** - * Represents an import in BoxLang + * Represents an import in BoxLang. Imports can have aliases, wildcards, and resolver prefixes. + *

+ * However, they can also be linked to a specific BoxLang Module via the {@code @{moduleName}} syntax. + *

+ * Normal Resolution: * + *

  * import prefix:package.to.Class as alias
  * import package.to.Class
  * import package.to.Class as alias
  * import package.to.*
+ * 
+ *

+ * Module Resolution: + * + *

+ * import package.to.Class@ModuleName as alias
+ * import package.to.Class@ModuleName
+ * import package.to.Class@ModuleName as alias
+ * import package.to.*@ModuleName
+ * 
* * @param className The class name * @param resolverPrefix The resolver prefix * @param alias The alias */ -public record ImportDefinition( String className, String resolverPrefix, String alias ) { +public record ImportDefinition( String className, String resolverPrefix, String alias, String moduleName ) { // Compact constructor disallows null className - public ImportDefinition - - { + public ImportDefinition { if ( className == null ) { throw new BoxRuntimeException( "Class name cannot be null." ); } @@ -51,6 +64,15 @@ public Boolean isMultiImport() { return className.endsWith( ".*" ); } + /** + * Is this import a module import? + * + * @return True if it is a module import, false otherwise + */ + public Boolean isModuleImport() { + return moduleName != null; + } + /** * Returns the package name of the import definition * @@ -62,7 +84,7 @@ public String getPackageName() { /** * Returns the fully qualified class name of the import definition - * considering if it is a wildcard import or not + * considering if it is a wildcard import or not, and if it's from a targeted module or not. * * @param targetClass The class name in code that needed qualification * @@ -71,9 +93,10 @@ public String getPackageName() { public String getFullyQualifiedClass( String targetClass ) { if ( isMultiImport() ) { return String.format( "%s.%s", className.substring( 0, className.length() - 2 ), targetClass ); - } else { - return className; + } else if ( isModuleImport() ) { + return String.format( "%s@%s", className, moduleName ); } + return className; } /** @@ -87,6 +110,7 @@ public static ImportDefinition parse( String importStr ) { String className = importStr; String resolverPrefix = null; String alias = null; + String module = null; int aliasDelimiterPos = importStr.toLowerCase().lastIndexOf( " as " ); if ( aliasDelimiterPos != -1 ) { @@ -100,6 +124,18 @@ public static ImportDefinition parse( String importStr ) { if ( alias.contains( "$" ) ) { alias = alias.substring( alias.lastIndexOf( "$" ) + 1 ); } + int moduleDelimiterPos = alias.indexOf( "@" ); + if ( moduleDelimiterPos != -1 ) { + alias = alias.substring( 0, moduleDelimiterPos ); + } + } + + // Check if the import is a module import, the class name must have a @moduleName + // Parse the module name and remove it from the class name + int moduleDelimiterPos = className.indexOf( "@" ); + if ( moduleDelimiterPos != -1 ) { + module = className.substring( moduleDelimiterPos + 1 ); + className = className.substring( 0, moduleDelimiterPos ); } int resolverDelimiterPos = className.indexOf( ":" ); @@ -108,6 +144,6 @@ public static ImportDefinition parse( String importStr ) { className = className.substring( resolverDelimiterPos + 1 ); } - return new ImportDefinition( className, resolverPrefix, alias ); + return new ImportDefinition( className, resolverPrefix, alias, module ); } } diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java index 5297cac40..6964f7f30 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java @@ -63,7 +63,7 @@ public class BaseResolver implements IClassResolver { /** * The import cache */ - private Set importCache = ConcurrentHashMap.newKeySet(); + protected Set importCache = ConcurrentHashMap.newKeySet(); /** * -------------------------------------------------------------------------- @@ -210,13 +210,15 @@ public Optional resolve( IBoxContext context, String name, List imports ) { var fullyQualifiedName = imports.stream() // Discover import by matching the resolver prefix and the class name or alias or multi-import - .filter( thisImport -> importApplies( thisImport ) && importHas( thisImport, className ) ) + // This runs from concrete resolvers: bx, java, etc. + // So if the resolver prefix matches, we continue, else we skip it. + .filter( thisImport -> importApplies( thisImport ) && importHas( context, thisImport, className ) ) // Return the first one, the first one wins .findFirst() // Convert the import to a fully qualified class name .map( targetImport -> { String fqn = targetImport.getFullyQualifiedClass( className ); - importCache.add( className + ":" + fqn ); + this.importCache.add( className + ":" + fqn ); return fqn; } ) // Nothing found, return the original class name @@ -231,35 +233,38 @@ public String expandFromImport( IBoxContext context, String className, List resolve( IBoxContext context, String name, List fullyQualifiedName: " + fullyQualifiedName ); return findFromModules( context, fullyQualifiedName, imports, loadClass ) .or( () -> findFromLocal( context, fullyQualifiedName, imports, loadClass ) ); @@ -167,64 +171,120 @@ public Optional resolve( IBoxContext context, String name, List findFromModules( IBoxContext context, String name, List imports ) { - return findFromModules( context, name, imports, true ); + public Optional findFromModules( IBoxContext context, String fullyQualifiedName, List imports ) { + return findFromModules( context, fullyQualifiedName, imports, true ); } /** - * Load a class from the registered runtime module class loaders + * This tries to load a BoxLang class from registered modules using the {@code fullyQualifiedName@moduleName} provided. + * If the class is not found, it will return an empty Optional. + *

+ * If there is no module name, then we return an empty Optional, because it will delegate to the {@link #findFromLocal} method, + * which will look for the class in the current template directory, or using module mappings. * + * + * @param context The current context of execution * @param name The fully qualified path of the class to load * @param imports The list of imports to use * @param loadClass When false, the class location is returned with informatino about where the class was found, but the class is not loaded and will be null. * * @return The loaded class or null if not found */ - public Optional findFromModules( IBoxContext context, String name, List imports, boolean loadClass ) { + public Optional findFromModules( IBoxContext context, String fullyQualifiedName, List imports, boolean loadClass ) { + // Do we have a explicit module name? path.to.Class@moduleName + String[] parts = fullyQualifiedName.split( "@" ); + + // If we have a module name, then we need to load the class from the module explicitly + if ( parts.length == 2 ) { + // fullyQualifiedName = parts[ 0 ]; + // moduleName = parts[ 1 ]; + return findFromModule( parts[ 0 ], Key.of( parts[ 1 ] ), imports, context ); + } + return Optional.ofNullable( null ); } + /** + * Find a class from a specific module explicitly. + * + * @param fullyQualifiedName The fully qualified path of the class to load in the module root + * @param moduleName The name of the module to look in + * @param imports The list of imports to use + * @param context The current context of execution + * + * @throws BoxRuntimeException If the module is not found + * + * @return The ClassLocation record wrapped in an optional if found, empty otherwise + */ + public Optional findFromModule( String fullyQualifiedName, Key moduleName, List imports, IBoxContext context ) { + ModuleService moduleService = BoxRuntime.getInstance().getModuleService(); + + // Verify the module exists, else throw up, as it was an explicit call + if ( !moduleService.hasModule( moduleName ) ) { + throw new BoxRuntimeException( + String.format( + "Module requested [%s] not found when looking for [%s]. Valid modules are: [%s]", + moduleName.getName(), + fullyQualifiedName, + moduleService.getModuleNames() + ) + ); + } + + // Get the module record and the physical path + ModuleRecord moduleRecord = moduleService.getModuleRecord( moduleName ); + String finalSlashName = getFullyQualifiedSlashName( fullyQualifiedName ); + + // See if path exists in this parent directory with a valid extension + Path targetPath = findExistingPathWithValidExtension( moduleRecord.physicalPath, finalSlashName ); + if ( targetPath != null ) { + ResolvedFilePath resolvedFilePath = ResolvedFilePath.of( targetPath ); + return Optional.of( new ClassLocation( + resolvedFilePath.getBoxFQN().getClassName(), + targetPath.toAbsolutePath().toString(), + resolvedFilePath.getBoxFQN().getPackageString(), + ClassLocator.TYPE_BX, + RunnableLoader.getInstance().loadClass( resolvedFilePath, context ), + moduleName.getName(), + false + ) ); + } + + return Optional.empty(); + } + /** * Load a class from the configured directory byte code * - * @param context The current context of execution - * @param name The fully qualified path of the class to load - * @param imports The list of imports to use + * @param context The current context of execution + * @param fullyQualifiedName The fully qualified path of the class to load + * @param imports The list of imports to use * * @return The loaded class or null if not found */ - public Optional findFromLocal( IBoxContext context, String name, List imports ) { - return findFromLocal( context, name, imports, true ); + public Optional findFromLocal( IBoxContext context, String fullyQualifiedName, List imports ) { + return findFromLocal( context, fullyQualifiedName, imports, true ); } /** * Load a class from the configured directory byte code * - * @param context The current context of execution - * @param name The fully qualified path of the class to load - * @param imports The list of imports to use - * @param loadClass When false, the class location is returned with informatino about where the class was found, but the class is not loaded and will be null. + * @param context The current context of execution + * @param fullyQualifiedName The fully qualified path of the class to load + * @param imports The list of imports to use + * @param loadClass When false, the class location is returned with informatino about where the class was found, but the class is not loaded and will be null. * * @return The loaded class or null if not found */ - public Optional findFromLocal( IBoxContext context, String name, List imports, boolean loadClass ) { - // Convert package dot name to a lookup path - String slashName = name.replace( "../", "DOT_DOT_SLASH" ) - .replace( ".", "/" ) - .replace( "DOT_DOT_SLASH", "../" ); - - // prepend / if not already present - if ( !slashName.startsWith( "/" ) ) { - slashName = "/" + slashName; - } - final String finalSlashName = slashName; - - // Find the class using: + public Optional findFromLocal( IBoxContext context, String fullyQualifiedName, List imports, boolean loadClass ) { + final String finalSlashName = getFullyQualifiedSlashName( fullyQualifiedName ); + // Try to find the class using: // 1. Relative to the current template // 2. A mapping return findByRelativeLocation( context, finalSlashName, name, imports, loadClass ) @@ -265,7 +325,7 @@ private Optional findByMapping( // Map it to a Stream object representing the paths to the classes .flatMap( entry -> { // Generate multiple paths here - List paths = new ArrayList(); + List paths = new ArrayList<>(); for ( String extension : getValidExtensions() ) { Path absolutePath = Path .of( StringUtils.replaceOnceIgnoreCase( slashName, entry.getKey().getName(), entry.getValue() + "/" ) + "." + extension ) @@ -345,10 +405,9 @@ private Optional findByRelativeLocation( if ( resolvedFilePath != null ) { Path template = resolvedFilePath.absolutePath(); - if ( template != null ) { + if ( template != null && !template.toString().equalsIgnoreCase( "unknown" ) ) { // Get the parent directory of the template, verify it exists, else we are done Path parentPath = template.getParent(); - // System.out.println( "parentPath: " + parentPath ); if ( parentPath != null ) { // See if path exists in this parent directory with a valid extension Path targetPath = findExistingPathWithValidExtension( parentPath, slashName ); @@ -370,6 +429,7 @@ private Optional findByRelativeLocation( } } } + return Optional.empty(); } @@ -393,4 +453,61 @@ private Path findExistingPathWithValidExtension( Path parentPath, String slashNa return null; } + /** + * This method will take the fully qualified class name and convert all + * periods (.) to (/) slashes. It will also prepend a slash (/) if it is not, + * already present. + * + * This is useful for converting a fully qualified class name to a path that + * can be used to look up a class in the file system. + * + * @param fullyQualifiedName The fully qualified class name to convert + * + * @return The fully qualified class name with periods converted to slashes + */ + private String getFullyQualifiedSlashName( String fullyQualifiedName ) { + // Convert package dot name to a lookup path + String slashName = fullyQualifiedName.replace( "../", "DOT_DOT_SLASH" ) + .replace( ".", "/" ) + .replace( "DOT_DOT_SLASH", "../" ); + + // prepend / if not already present + if ( !slashName.startsWith( "/" ) ) { + slashName = "/" + slashName; + } + + return slashName; + } + + /** + * Checks if the import has the given class name as a multi-import + * + * @param context The current context of execution + * @param thisImport The import to check + * @param className The class name to check + * + * @return True if the import has the class name, false otherwise + */ + @Override + public boolean importHasMulti( IBoxContext context, ImportDefinition thisImport, String className ) { + String packageSlashName = getFullyQualifiedSlashName( thisImport.getPackageName() ); + + // This verifies that the package exists, else we need to expand it + if ( !FileSystemUtil.exists( packageSlashName ) ) { + packageSlashName = FileSystemUtil.expandPath( context, packageSlashName ).absolutePath().toString(); + } + + // Get the stream of class files in the package + // If it finds a class that matches the class name, then it returns true, else false + return FileSystemUtil.listDirectory( + packageSlashName, + false, + null, + "name", + "file" + ) + .anyMatch( path -> path.getFileName().toString().startsWith( className + "." ) ); + + } + } diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java index 614e03e8e..ff3c346e9 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java @@ -265,16 +265,17 @@ public Optional findFromSystem( String fullyQualifiedName, List getClassFilesAsStream( String packageName, Boolean recursive ) throws IOException { ClassLoader classLoader = BoxRuntime.getInstance().getClass().getClassLoader(); String path = packageName.replace( '.', '/' ); @@ -77,9 +78,16 @@ public static Stream getClassFilesAsStream( String packageName, Boolean return Collections.list( resources ) .stream() - .map( URL::getFile ) - .map( File::new ) - .flatMap( directory -> Stream.of( findClassNames( directory, packageName, recursive ) ) ); + .flatMap( url -> { + // We do different things based on the protocol + if ( url.getProtocol().equals( JAR_FILE_EXTENSION ) ) { + List> classesFound = findClassesInJar( url, path, classLoader, new Class[ 0 ] ); + return classesFound.stream().map( clazz -> clazz.getName() ); + } else { + File directory = new File( url.getFile() ); + return Stream.of( findClassNames( directory, packageName, recursive ) ); + } + } ); } /** @@ -273,7 +281,13 @@ public static Path getPathFromResource( String resourceName ) { * @throws ClassNotFoundException */ public static String[] findClassNames( File directory, String packageName, Boolean recursive ) { - return Arrays.stream( directory.listFiles() ) + // Do we have anything? + File[] aClassNames = directory.listFiles(); + if ( aClassNames == null ) { + return new String[ 0 ]; + } + + return Arrays.stream( aClassNames ) .parallel() .flatMap( file -> { if ( file.isDirectory() ) { diff --git a/src/main/java/ortus/boxlang/runtime/scopes/Key.java b/src/main/java/ortus/boxlang/runtime/scopes/Key.java index cef36f56d..b6aff2b05 100644 --- a/src/main/java/ortus/boxlang/runtime/scopes/Key.java +++ b/src/main/java/ortus/boxlang/runtime/scopes/Key.java @@ -123,7 +123,7 @@ public class Key implements Comparable, Serializable { public static final Key boxBif = Key.of( "BoxBif" ); public static final Key boxCacheProvider = Key.of( "BoxCacheProvider" ); public static final Key boxlang = Key.of( "boxlang" ); - public static final Key boxlangSessions = Key.of( "boxlangSessions" ); + public static final Key bxSessions = Key.of( "bxSessions" ); public static final Key boxMember = Key.of( "BoxMember" ); public static final Key boxRuntime = Key.of( "boxRuntime" ); public static final Key buffersize = Key.of( "buffersize" ); diff --git a/src/main/java/ortus/boxlang/runtime/types/Array.java b/src/main/java/ortus/boxlang/runtime/types/Array.java index f2577cb8a..6e50d7536 100644 --- a/src/main/java/ortus/boxlang/runtime/types/Array.java +++ b/src/main/java/ortus/boxlang/runtime/types/Array.java @@ -139,15 +139,6 @@ public Array( Object[] arr ) { this.wrapped = Collections.synchronizedList( new ArrayList( Arrays.asList( arr ) ) ); } - /** - * Constructor to create an Array from a Java byte array - * - * @param arr The array to create the Array from - */ - public Array( byte[] arr ) { - this.wrapped = Collections.synchronizedList( new ArrayList( Arrays.asList( arr ) ) ); - } - /** * Constructor to create an Array from a List * @@ -216,7 +207,7 @@ public static Array fromSet( Set set ) { } /** - * Create an Array from a Java array + * Create an Array from a Java array of boxed objects * * @param arr The array to create the Array from */ diff --git a/src/main/java/ortus/boxlang/runtime/types/util/MapHelper.java b/src/main/java/ortus/boxlang/runtime/types/util/MapHelper.java index 451d45d77..32bf79651 100644 --- a/src/main/java/ortus/boxlang/runtime/types/util/MapHelper.java +++ b/src/main/java/ortus/boxlang/runtime/types/util/MapHelper.java @@ -27,6 +27,24 @@ public class MapHelper { + /** + * Create a LinkedHashMap from a list of values. The values must be in pairs, key, value, key, value, etc. + * + * @param values The values to create the struct from + * + * @return The struct + */ + public static Map LinkedHashMapOfAny( Object... values ) { + if ( values.length % 2 != 0 ) { + throw new BoxRuntimeException( "Invalid number of arguments. Must be an even number." ); + } + var map = new LinkedHashMap(); + for ( int i = 0; i < values.length; i += 2 ) { + map.put( ( Key ) values[ i ], ( Object ) values[ i + 1 ] ); + } + return map; + } + /** * Create a LinkedHashMap from a list of values. The values must be in pairs, key, value, key, value, etc. * diff --git a/src/main/java/ortus/boxlang/runtime/util/DumpUtil.java b/src/main/java/ortus/boxlang/runtime/util/DumpUtil.java index 418897919..12f9c2d26 100644 --- a/src/main/java/ortus/boxlang/runtime/util/DumpUtil.java +++ b/src/main/java/ortus/boxlang/runtime/util/DumpUtil.java @@ -242,7 +242,11 @@ public static void dumpToConsole( IBoxContext context, Object target, String lab } } else { // out.println( "> " + target.getClass().getName() ); - out.println( target.toString() ); + if ( target == null ) { + out.println( "[null]" ); + } else { + out.println( target.toString() ); + } } } @@ -253,7 +257,11 @@ public static void dumpToConsole( IBoxContext context, Object target, String lab * @param target The target object */ public static void dumpTextToBuffer( IBoxContext context, Object target ) { - context.writeToBuffer( target.toString(), true ); + if ( target == null ) { + context.writeToBuffer( "[null]", true ); + } else { + context.writeToBuffer( target.toString(), true ); + } } /** diff --git a/src/main/java/ortus/boxlang/runtime/util/FileSystemUtil.java b/src/main/java/ortus/boxlang/runtime/util/FileSystemUtil.java index 008caecd2..ee130fc81 100644 --- a/src/main/java/ortus/boxlang/runtime/util/FileSystemUtil.java +++ b/src/main/java/ortus/boxlang/runtime/util/FileSystemUtil.java @@ -282,20 +282,23 @@ public static void deleteDirectory( String directoryPath, Boolean recursive ) { * * @param path the path to list * @param recurse whether to recurse into subdirectories - * @param filter a glob filter or a closure to filter the results + * @param filter a glob filter or a closure to filter the results as a Predicate * @param sort a string containing the sort field and direction * @param type the type of files to list * - * @return + * @return a stream of paths */ @SuppressWarnings( "unchecked" ) public static Stream listDirectory( String path, Boolean recurse, Object filter, String sort, String type ) { - final String theType = type.toLowerCase(); + Path targetPath = Path.of( path ); + // If path doesn't exist, return an empty stream - if ( !Files.exists( Path.of( path ) ) ) { + if ( !Files.exists( targetPath ) ) { return Stream.empty(); } + // Setup variables + final String theType = type.toLowerCase(); String[] sortElements = sort.split( ( "\\s+" ) ); String sortField = sortElements[ 0 ]; String sortDirection = sortElements.length > 1 ? sortElements[ 1 ].toLowerCase() : "asc"; @@ -333,32 +336,47 @@ public static Stream listDirectory( String path, Boolean recurse, Object f try { if ( recurse ) { - directoryStream = Files.walk( Path.of( path ) ).parallel().filter( filterPath -> !filterPath.equals( Path.of( path ) ) ); + directoryStream = Files.walk( targetPath ).parallel().filter( filterPath -> !filterPath.equals( targetPath ) ); } else { - directoryStream = Files.walk( Path.of( path ), 1 ).parallel().filter( filterPath -> !filterPath.equals( Path.of( path ) ) ); + directoryStream = Files.walk( targetPath, 1 ).parallel().filter( filterPath -> !filterPath.equals( targetPath ) ); } } catch ( IOException e ) { throw new BoxIOException( e ); } + // Apply the type filter directoryStream = directoryStream.filter( item -> matchesType( item, theType ) ); - if ( filter instanceof String && StringCaster.cast( filter ).length() > 1 ) { - ArrayList pathMatchers = ListUtil.asList( StringCaster.cast( filter ), "|" ) + // Is the filter a string or a closure? + if ( filter instanceof String castedFilter && castedFilter.length() > 1 ) { + ArrayList pathMatchers = ListUtil + .asList( castedFilter, "|" ) .stream() .map( filterString -> FileSystems.getDefault().getPathMatcher( "glob:" + filterString ) ) .collect( Collectors.toCollection( ArrayList::new ) ); directoryStream = directoryStream.filter( item -> pathMatchers.stream().anyMatch( pathMatcher -> pathMatcher.matches( item.getFileName() ) ) ); - } else if ( filter instanceof java.util.function.Predicate ) { + } + // Predicate filter + else if ( filter instanceof java.util.function.Predicate ) { directoryStream = directoryStream.filter( ( java.util.function.Predicate ) filter ); } + // Finally, sort the stream return directoryStream.sorted( pathSort ); } + /** + * Matches the type of a file or directory + * + * @param item the path to match + * @param type the type to match + * + * @return a boolean as to whether the path matches the type + */ private static Boolean matchesType( Path item, String type ) { switch ( type ) { case "directory" : + case "dir" : return Files.isDirectory( item ); case "file" : return Files.isRegularFile( item ); @@ -1079,9 +1097,9 @@ public static Object deserializeFromFile( Path filePath ) { /** * Performs case insensitive path resolution. This will return the real path, which may be different in case than the incoming path. - * + * * @param path The path to check - * + * * @return The resolved path or null if not found */ public static Path pathExistsCaseInsensitive( Path path ) { diff --git a/src/main/java/ortus/boxlang/runtime/util/ResolvedFilePath.java b/src/main/java/ortus/boxlang/runtime/util/ResolvedFilePath.java index 8b311de24..d768583e9 100644 --- a/src/main/java/ortus/boxlang/runtime/util/ResolvedFilePath.java +++ b/src/main/java/ortus/boxlang/runtime/util/ResolvedFilePath.java @@ -17,9 +17,12 @@ */ package ortus.boxlang.runtime.util; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import ortus.boxlang.runtime.types.exceptions.BoxIOException; + /** * I represent the a file path that has been resolved to an absolute path. * I track additional data such as what mapping was used to resolve the path. @@ -42,7 +45,7 @@ public static ResolvedFilePath of( String mappingName, String mappingPath, Strin mappingName, mappingPath, relativePath, - absolutePath != null ? absolutePath.normalize() : null + absolutePath != null ? makeReal( absolutePath.normalize() ) : null ); } @@ -77,7 +80,7 @@ public static ResolvedFilePath of( Path absolutePath ) { null, null, absolutePath != null ? absolutePath.normalize().toString() : null, - absolutePath + makeReal( absolutePath ) ); } @@ -97,7 +100,7 @@ public static ResolvedFilePath of( String absolutePath ) { * * @return true if the path was resolved via a mapping. */ - public boolean resovledViaMapping() { + public boolean resolvedViaMapping() { return mappingName != null; } @@ -169,4 +172,16 @@ public ResolvedFilePath newFromRelative( String relativePath ) { ); } + private static Path makeReal( Path path ) { + // if exists, make it real + if ( path != null && path.toFile().exists() ) { + try { + return path.toRealPath(); + } catch ( IOException e ) { + throw new BoxIOException( e ); + } + } + return path; + } + } diff --git a/src/main/java/ortus/boxlang/runtime/validation/NotImplemented.java b/src/main/java/ortus/boxlang/runtime/validation/NotImplemented.java index 2dc850322..ed41a6feb 100644 --- a/src/main/java/ortus/boxlang/runtime/validation/NotImplemented.java +++ b/src/main/java/ortus/boxlang/runtime/validation/NotImplemented.java @@ -30,9 +30,18 @@ public class NotImplemented implements Validator { public void validate( IBoxContext context, Key caller, Validatable record, IStruct records ) { - if ( records.get( record.name() ) != null ) { + Object targetValue = records.get( record.name() ); + + if ( targetValue != null ) { + + // If the value is a string and EMPTY, then ignore it + // LEEWAY! + if ( targetValue instanceof String castedTargetValue && castedTargetValue.isEmpty() ) { + return; + } + throw new BoxValidationException( caller, record, "is not implemented yet and should not be provided." ); } } -} \ No newline at end of file +} diff --git a/src/main/resources/META-INF/boxlang/license.txt b/src/main/resources/META-INF/boxlang/license.txt index 8e741b018..e3a27f664 100755 --- a/src/main/resources/META-INF/boxlang/license.txt +++ b/src/main/resources/META-INF/boxlang/license.txt @@ -1,6 +1,6 @@ ******************************************************************************** BoxLang Copyright Since 2023 by Ortus Solutions, Corp -www.boxlang.org | www.ortussolutions.com +www.boxlang.io | www.ortussolutions.com ******************************************************************************** Ways to Help: - Become a Sponsor of the project by visiting https://www.ortussolutions.com/about-us/sponsors diff --git a/src/main/resources/config/boxlang.json b/src/main/resources/config/boxlang.json index 932598757..f84a0dfad 100644 --- a/src/main/resources/config/boxlang.json +++ b/src/main/resources/config/boxlang.json @@ -74,7 +74,13 @@ "logsDirectory": "${boxlang-home}/logs", // This is the experimental features flags. // Please see the documentation to see which flags are available - "experimental": {}, + "experimental": { + // This choose the compiler to use for the runtime + // Valid values are: "java", "asm" + "compiler": "java", + // If enabled, it will generate AST JSON data under the project's /grapher/data folder + "ASTCapture": false + }, // Global Executors for the runtime // These are managed by the AsyncService and registered upon startup // The name of the executor is the key and the value is a struct of executor settings @@ -107,87 +113,43 @@ // "database": "test" // } }, - // An explicit whitelist of file extensions that are allowed to be uploaded - overrides any values in the disallowedWriteExtensions - "allowedFileOperationExtensions": [], - // The list of file extensions that are not allowed to be uploaded. Also enforced by file relocation operations ( e.g. copy/move ) - "disallowedFileOperationExtensions": [ - "bat", - "exe", - "cmd", - "cfm", - "cfc", - "cfs", - "bx", - "bxm", - "bxs", - "sh", - "php", - "pl", - "cgi", - "386", - "dll", - "com", - "torrent", - "js", - "app", - "jar", - "pif", - "vb", - "vbscript", - "wsf", - "asp", - "cer", - "csr", - "jsp", - "drv", - "sys", - "ade", - "adp", - "bas", - "chm", - "cpl", - "crt", - "csh", - "fxp", - "hlp", - "hta", - "inf", - "ins", - "isp", - "jse", - "htaccess", - "htpasswd", - "ksh", - "lnk", - "mdb", - "mde", - "mdt", - "mdw", - "msc", - "msi", - "msp", - "mst", - "ops", - "pcd", - "prg", - "reg", - "scr", - "sct", - "shb", - "shs", - "url", - "vbe", - "vbs", - "wsc", - "wsf", - "wsh" - ], // The default return format for class invocations via web runtimes "defaultRemoteMethodReturnFormat": "json", // The configuration for the BoxLang `default` cache. If empty, we use the defaults // See the ortus.boxlang.runtime.config.segments.CacheConfig for all the available settings - // This is used by query caching, template caching, and other internal caching, unless you override it - "defaultCache": {}, + // This is used by query caching, template caching, and other internal caching. + // You can use the cache() BIF in order to get access to the default cache. + "defaultCache": { + // How many to evict at a time once a policy is triggered + "evictCount": 1, + // The eviction policy to use: Least Recently Used + // Other policies are: LRU, LFU, FIFO, LIFO, RANDOM + "evictionPolicy": "LRU", + // The free memory percentage threshold to trigger eviction + // 0 = disabled, 1-100 = percentage of available free memory in heap + // If the threadhold is reached, the eviction policy is triggered + "freeMemoryPercentageThreshold": 0, + // The maximum number of objects to store in the cache + "maxObjects": 1000, + // The maximum in seconds to keep an object in the cache since it's last access + // So if an object is not accessed in this time or greater, it will be removed from the cache + "defaultLastAccessTimeout": 1800, + // The maximum time in seconds to keep an object in the cache regardless if it's used or not + // A default timeout of 0 = never expire, careful with this setting + "defaultTimeout": 3600, + // The object store to use to store the objects. + // The default is a ConcurrentStore which is a memory sensitive store + "objectStore": "ConcurrentStore", + // The frequency in seconds to check for expired objects and expire them using the policy + // This creates a BoxLang task that runs every X seconds to check for expired objects + "reapFrequency": 120, + // If enabled, the last access timeout will be reset on every access + // This means that the last access timeout will be reset to the defaultLastAccessTimeout on every access + // Usually for session caches or to simulate a session + "resetTimeoutOnAccess": false, + // If enabled, the last access timeout will be used to evict objects from the cache + "useLastAccessTimeouts": true + }, /** * Register any named caches here. * The key is the name of the cache and the value is the cache configuration. @@ -196,7 +158,39 @@ * The `properties` property is optional and is a struct of properties that are specific to the cache provider. */ "caches": { - "imports": { + // This is the holder of all sessions in a BoxLang runtime. + // The keys are prefixed by application to create separation. + "bxSessions": { + "provider": "BoxCacheProvider", + "properties": { + // How many objects to evict when the cache is full + "evictCount": 1, + // The eviction policy to use: FIFO, LFU, LIFO, LRU, MFU, MRU, Random + "evictionPolicy": "LRU", + // The maximum number of objects the cache can hold + "maxObjects": 100000, + // How long should sessions last for in seconds. Default is 60 minutes. + "defaultTimeout": 3600, + // The object store to use to store the objects. + // The default is a ConcurrentStore which is a thread safe and fast storage. + // Available Stores are: BlackHoleStore, ConcurrentSoftReferenceStore, ConcurrentStore, FileSystemStore, Your own. + "objectStore": "ConcurrentStore", + // The free memory percentage threshold to start evicting objects + // Only use if memory is constricted and you need to relieve cache pressure + // Please note that this only makes sense depending on which object store you use. + "freeMemoryPercentageThreshold": 0, + // The frequency in seconds to check for expired objects and expire them using the policy + // This creates a BoxLang task that runs every X seconds to check for expired objects + // Default is every 2 minutes + "reapFrequency": 120, + // This makes a session extend it's life when accessed. So if a users uses anything or puts anything + // In session, it will re-issue the timeout. + "resetTimeoutOnAccess": true, + // Sessions don't rely on the last access timeouts but on the default timeout only. + "useLastAccessTimeouts": false + } + }, + "bxImports": { "provider": "BoxCacheProvider", "properties": { "evictCount": 1, @@ -224,7 +218,82 @@ "disallowedBifs": [], // A list of Component names that will be disallowed from execution // Ex: "disallowedComponents": [ "execute", "http" ] - "disallowedComponents": [] + "disallowedComponents": [], + // An explicit whitelist of file extensions that are allowed to be uploaded - overrides any values in the disallowedWriteExtensions + "allowedFileOperationExtensions": [], + // The list of file extensions that are not allowed to be uploaded. Also enforced by file relocation operations ( e.g. copy/move ) + "disallowedFileOperationExtensions": [ + "bat", + "exe", + "cmd", + "cfm", + "cfc", + "cfs", + "bx", + "bxm", + "bxs", + "sh", + "php", + "pl", + "cgi", + "386", + "dll", + "com", + "torrent", + "js", + "app", + "jar", + "pif", + "vb", + "vbscript", + "wsf", + "asp", + "cer", + "csr", + "jsp", + "drv", + "sys", + "ade", + "adp", + "bas", + "chm", + "cpl", + "crt", + "csh", + "fxp", + "hlp", + "hta", + "inf", + "ins", + "isp", + "jse", + "htaccess", + "htpasswd", + "ksh", + "lnk", + "mdb", + "mde", + "mdt", + "mdw", + "msc", + "msi", + "msp", + "mst", + "ops", + "pcd", + "prg", + "reg", + "scr", + "sct", + "shb", + "shs", + "url", + "vbe", + "vbs", + "wsc", + "wsf", + "wsh" + ] }, /** * The BoxLang module settings diff --git a/src/test/bx/index.bxm b/src/test/bx/index.bxm new file mode 100644 index 000000000..df3a3dfe7 --- /dev/null +++ b/src/test/bx/index.bxm @@ -0,0 +1,3 @@ + + This is a test template + diff --git a/src/test/java/TestCases/asm/integration/ControllerTest.java b/src/test/java/TestCases/asm/integration/ControllerTest.java index dbc536db8..494c6cd9d 100644 --- a/src/test/java/TestCases/asm/integration/ControllerTest.java +++ b/src/test/java/TestCases/asm/integration/ControllerTest.java @@ -17,6 +17,8 @@ */ package TestCases.asm.integration; +import static com.google.common.truth.Truth.assertThat; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -60,12 +62,117 @@ public void teardownEach() { } @Test - public void testControllerCompilation() { + public void testSwitchInLoopInFunc() { + instance.executeStatement( + """ + function a(){ + for ( x = 1; x < 5; x++ ) { + switch( x ){ + } + } + } + """, + context ); + } + + @Test + public void testSwitchWithCaseInLoopInFunc() { instance.executeStatement( """ - controller = new src.test.java.TestCases.asm.integration.Controller(); + function a(){ + for ( x = 1; x < 5; x++ ) { + switch( x ){ + case "id": { + + } + } + } + + } + a() + """, + context ); + } + + @Test + public void testInterfaceImplementation() { + instance.executeStatement( + """ + impl = new src.test.java.TestCases.asm.integration.Implementor(); + + impl.setName( "test" ); + + result = impl.getName(); + """, + context ); + + assertThat( variables.get( result ) ).isEqualTo( "test" ); + } + + @Test + public void testTryCatchLabelStack() { + + instance.executeStatement( + """ + controller = new src.test.java.TestCases.asm.integration.TryCatchLabelStack(); """, context ); } + @Test + public void testNestedLFunctionInComponent() { + + instance.executeStatement( + """ + lock name = "what" timeout=300 { + t = function(){ + return "test"; + } + + result = t(); + } + """, + context ); + + assertThat( variables.get( result ) ).isEqualTo( "test" ); + } + + @Test + public void testReturnFromComponentInFunction() { + + instance.executeStatement( + """ + function a(){ + lock name="what" timeout=300 { + return "test"; + } + } + + result = a(); + """, + context ); + + assertThat( variables.get( result ) ).isEqualTo( "test" ); + } + + @Test + public void testContinueInForIndex() { + + instance.executeStatement( + """ + a = [ "orange", "red", "yellow" ] + + for( color in a ){ + + switch( color ){ + case "orange": result = color; break; + default: continue; + } + } + """, + context ); + + assertThat( variables.get( result ) ).isEqualTo( "orange" ); + } + } diff --git a/src/test/java/TestCases/asm/integration/ICacheProvider.cfc b/src/test/java/TestCases/asm/integration/ICacheProvider.cfc new file mode 100644 index 000000000..4f750cc85 --- /dev/null +++ b/src/test/java/TestCases/asm/integration/ICacheProvider.cfc @@ -0,0 +1,26 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * + * The main interface for a CacheBox cache provider. You need to implement all the methods in order for CacheBox to work correctly for the implementing cache provider. + * + * @author Luis Majano + */ +interface { + + /** + * Get the name of this cache + */ + function getName(); + + /** + * Set the cache name + * + * @name The name to set + * + * @return ICacheProvider + */ + function setName( required name ); + +} diff --git a/src/test/java/TestCases/asm/integration/Implementor.cfc b/src/test/java/TestCases/asm/integration/Implementor.cfc new file mode 100644 index 000000000..9283f17c2 --- /dev/null +++ b/src/test/java/TestCases/asm/integration/Implementor.cfc @@ -0,0 +1,12 @@ +component implements="ICacheProvider" { + + variables.name = ""; + + function getName(){ + return variables.name; + } + + function setName( required name ){ + variables.name = name; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/integration/TryCatchLabelStack.cfc b/src/test/java/TestCases/asm/integration/TryCatchLabelStack.cfc new file mode 100644 index 000000000..0e655390c --- /dev/null +++ b/src/test/java/TestCases/asm/integration/TryCatchLabelStack.cfc @@ -0,0 +1,8 @@ +component { + try { + } + + savecontent variable="x" { + "test"; + } +} diff --git a/src/test/java/TestCases/asm/phase1/CoreLangTest.java b/src/test/java/TestCases/asm/phase1/CoreLangTest.java index f02b6736c..c68f12d5d 100644 --- a/src/test/java/TestCases/asm/phase1/CoreLangTest.java +++ b/src/test/java/TestCases/asm/phase1/CoreLangTest.java @@ -1188,6 +1188,29 @@ public void testSwtich() { } + @Test + public void testSwtichWithBreakInDefault() { + + instance.executeSource( + """ + result = "" + variables.foo = true; + + switch( "12" ) { + case "brad": + result= "brad"; + break; + default: + result="test"; + break; + } + """, + context ); + + assertThat( variables.get( result ) ).isEqualTo( "test" ); + + } + @DisplayName( "switch fall through case" ) @Test public void testSwtichFallThroughCase() { diff --git a/src/test/java/TestCases/asm/phase1/LabeledLoopTest.java b/src/test/java/TestCases/asm/phase1/LabeledLoopTest.java index af32a6fba..18fa1333e 100644 --- a/src/test/java/TestCases/asm/phase1/LabeledLoopTest.java +++ b/src/test/java/TestCases/asm/phase1/LabeledLoopTest.java @@ -154,7 +154,7 @@ public void testSimpleLabeledForIn() { """ data = [1,2,3] result = 0 - mylabel : for( var x in data ) { + mylabel : for( x in data ) { result ++ break mylabel; result ++ diff --git a/src/test/java/TestCases/asm/phase1/OperatorsTest.java b/src/test/java/TestCases/asm/phase1/OperatorsTest.java index 065b6069d..5b0bacd61 100644 --- a/src/test/java/TestCases/asm/phase1/OperatorsTest.java +++ b/src/test/java/TestCases/asm/phase1/OperatorsTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import ortus.boxlang.compiler.parser.BoxSourceType; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; @@ -929,4 +930,38 @@ public void testIMPOperator() { } + @DisplayName( "comparison strict not equality" ) + @Test + public void testComparisonStrictNotEquality() { + + Object result = instance.executeStatement( "5!==5", context ); + assertThat( result ).isEqualTo( false ); + + result = instance.executeStatement( "'5'!==5", context ); + assertThat( result ).isEqualTo( true ); + + result = instance.executeStatement( "'brad'!=='brad'", context ); + assertThat( result ).isEqualTo( false ); + + result = instance.executeStatement( "'brad'!==5", context ); + assertThat( result ).isEqualTo( true ); + } + + @DisplayName( "comparison strict not equality CF" ) + @Test + public void testComparisonStrictNotEqualityCF() { + + Object result = instance.executeStatement( "5!==5", context, BoxSourceType.CFSCRIPT ); + assertThat( result ).isEqualTo( false ); + + result = instance.executeStatement( "'5'!==5", context, BoxSourceType.CFSCRIPT ); + assertThat( result ).isEqualTo( true ); + + result = instance.executeStatement( "'brad'!=='brad'", context, BoxSourceType.CFSCRIPT ); + assertThat( result ).isEqualTo( false ); + + result = instance.executeStatement( "'brad'!==5", context, BoxSourceType.CFSCRIPT ); + assertThat( result ).isEqualTo( true ); + } + } diff --git a/src/test/java/TestCases/asm/phase3/AbstractClass.bx b/src/test/java/TestCases/asm/phase3/AbstractClass.bx new file mode 100644 index 000000000..af9af4554 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/AbstractClass.bx @@ -0,0 +1,9 @@ +abstract class { + + function normal() { + return "normal"; + } + + abstract function abstractMethod() + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/AbstractClassCF.cfc b/src/test/java/TestCases/asm/phase3/AbstractClassCF.cfc new file mode 100644 index 000000000..dc3d808e4 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/AbstractClassCF.cfc @@ -0,0 +1,9 @@ +abstract component { + + function normal() { + return "normal"; + } + + abstract function abstractMethod() + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Animal.cfc b/src/test/java/TestCases/asm/phase3/Animal.cfc new file mode 100644 index 000000000..7fc908207 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Animal.cfc @@ -0,0 +1,36 @@ +component { + variables.results = []; + results.append('animal pseudo ' & getFileFromPath( getCurrentTemplatePath())) + variables.inAnimal = true; + variables.inDog = false; + + function init() { + results.append('Animal init ' & getFileFromPath( getCurrentTemplatePath())) + } + + function speak() { + throw( "speak method not implemented" ); + } + + function isWarmBlooded() { + // this needs to be a reference to the bottom most class + results.append( "animal this is: " & this.$bx.meta.name ) + // We need to see the variables scope of the bottom most class + results.append( "animal sees inDog as: " & inDog ) + return true; + } + + function getScientificName() { + // this needs to be a reference to the bottom most class + results.append( "super animal sees: " & this.$bx.meta.name ) + // We need to see the variables scope of the bottom most class + results.append( "super sees inDog as: " & inDog ) + + return "Animal Kingdom"; + } + + function getResults() { + return results; + } + + } diff --git a/src/test/java/TestCases/asm/phase3/ApplicationTest.java b/src/test/java/TestCases/asm/phase3/ApplicationTest.java new file mode 100644 index 000000000..989e30a58 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ApplicationTest.java @@ -0,0 +1,242 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package TestCases.asm.phase3; + +import static com.google.common.truth.Truth.assertThat; + +import java.nio.file.Path; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.application.Application; +import ortus.boxlang.runtime.application.BaseApplicationListener; +import ortus.boxlang.runtime.context.ApplicationBoxContext; +import ortus.boxlang.runtime.context.BaseBoxContext; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.RequestBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.dynamic.casters.DateTimeCaster; +import ortus.boxlang.runtime.scopes.ApplicationScope; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.SessionScope; +import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.IStruct; + +public class ApplicationTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + instance.useASMBoxPiler(); + } + + @AfterEach + public void teardownEach() { + instance.useJavaBoxpiler(); + } + + @DisplayName( "application basics" ) + @Test + public void testBasicApplication() { + // @formatter:off + instance.executeSource( + """ + application name="myAppsdfsdf" sessionmanagement="true"; + + result = application; + result2 = session; + startTime = ApplicationStartTime() + """, context ); + // @formatter:on + + assertThat( variables.get( result ) ).isInstanceOf( ApplicationScope.class ); + assertThat( variables.get( Key.of( "result2" ) ) ).isInstanceOf( SessionScope.class ); + + ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); + Application app = appContext.getApplication(); + Instant actual = DateTimeCaster.cast( variables.get( Key.of( "startTime" ) ) ).getWrapped().toInstant(); + Instant now = Instant.now(); + long differenceInSeconds = ChronoUnit.SECONDS.between( actual, now ); + + assertThat( app.getName().getName() ).isEqualTo( "myAppsdfsdf" ); + assertThat( app.getSessionsCache() ).isNotNull(); + assertThat( app.getApplicationScope() ).isNotNull(); + assertThat( app.getApplicationScope().getName().getName() ).isEqualTo( "application" ); + assertThat( app.getClassLoaders() ).isNotNull(); + assertThat( app.hasStarted() ).isTrue(); + assertThat( differenceInSeconds ).isAtMost( 1L ); + } + + @Test + public void testGetAppMeta() { + // @formatter:off + instance.executeSource( + """ + application name="myAppsdfsdf2" sessionmanagement="true"; + result = GetApplicationMetadata(); + """, context ); + // @formatter:on + + assertThat( variables.get( result ) ).isInstanceOf( IStruct.class ); + assertThat( variables.getAsStruct( result ).get( "name" ) ).isEqualTo( "myAppsdfsdf2" ); + assertThat( variables.getAsStruct( result ).get( "sessionmanagement" ).toString() ).isEqualTo( "true" ); + } + + @Test + public void testGetDefaultAppMeta() { + // @formatter:off + instance.executeSource( + """ + result = GetApplicationMetadata(); + """, context ); + // @formatter:on + + assertThat( variables.get( result ) ).isInstanceOf( IStruct.class ); + assertThat( variables.getAsStruct( result ).get( "name" ) ).isEqualTo( "" ); + assertThat( variables.getAsStruct( result ).get( "sessionmanagement" ).toString() ).isEqualTo( "false" ); + } + + @DisplayName( "java settings setup" ) + @Test + public void testJavaSettings() { + // @formatter:off + instance.executeSource( + """ + application name="myJavaApp" javaSettings={ + loadPaths = [ "/src/test/resources/libs" ], + reloadOnChange = true + }; + + import com.github.benmanes.caffeine.cache.Caffeine + targetInstance = Caffeine.newBuilder() + + import org.apache.commons.lang3.ClassUtils + targetInstance2 = ClassUtils.getClass() + """, context ); + // @formatter:on + + ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); + Application app = appContext.getApplication(); + assertThat( app.getClassLoaderCount() ).isEqualTo( 1 ); + } + + @DisplayName( "Ad-hoc config override" ) + @Test + public void testAdHocConfigOverride() { + + context.injectParentContext( new BaseBoxContext() { + + public IStruct getConfig() { + IStruct config = super.getConfig(); + config.put( "adHocConfig", "adHocConfigValue" ); + return config; + } + } ); + + assertThat( context.getConfigItem( Key.of( "adHocConfig" ) ) ).isEqualTo( "adHocConfigValue" ); + } + + @DisplayName( "Can resolve java settings paths with a full jar/class path" ) + @Test + public void testJavaSettingsPaths() { + // @formatter:off + instance.executeSource( + """ + application name="myJavaApp" javaSettings={ + loadPaths = [ "/src/test/resources/libs/helloworld.jar" ], + reloadOnChange = true + }; + """, context ); + // @formatter:on + + ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); + Application app = appContext.getApplication(); + assertThat( app.getClassLoaderCount() ).isEqualTo( 1 ); + } + + @DisplayName( "Can resolve java settings paths with a full jar/class path with bad pathing" ) + @Test + public void testJavaSettingsBadPaths() { + // @formatter:off + instance.executeSource( + """ + application name="myJavaApp" javaSettings={ + loadPaths = [ "\\src\\test\\resources\\libs\\helloworld.jar" ], + reloadOnChange = true + }; + """, context ); + // @formatter:on + + ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); + Application app = appContext.getApplication(); + + assertThat( app.getClassLoaderCount() ).isEqualTo( 1 ); + } + + @DisplayName( "Can resolve relative paths" ) + @Test + public void testJavaSettingsRelativePaths() { + + RequestBoxContext requestContext = context.getParentOfType( RequestBoxContext.class ); + BaseApplicationListener listener = requestContext.getApplicationListener(); + + // Mock the relative path + listener.getSettings() + .put( "source", Path.of( "src/test/resources/Application.bx" ).toAbsolutePath().toString() ); + + // @formatter:off + instance.executeSource( + """ + application name="myJavaApp" javaSettings={ + loadPaths = [ "libs/helloworld.jar" ], + reloadOnChange = true + }; + """, context ); + // @formatter:on + + ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); + Application app = appContext.getApplication(); + assertThat( app.getClassLoaderCount() ).isEqualTo( 1 ); + } + +} diff --git a/src/test/java/TestCases/asm/phase3/CFImportTest.cfc b/src/test/java/TestCases/asm/phase3/CFImportTest.cfc new file mode 100644 index 000000000..1bcae0686 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/CFImportTest.cfc @@ -0,0 +1,13 @@ +import src.test.java.TestCases.phase3.Stack; + +component { + function init() { + stack = new Stack(); + } + + function doSomething() { + if (!stack.empty() ) { + // do stuff + } + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/CFImportTest2.cfc b/src/test/java/TestCases/asm/phase3/CFImportTest2.cfc new file mode 100644 index 000000000..6aff56356 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/CFImportTest2.cfc @@ -0,0 +1,13 @@ +import "src.test.java.TestCases.phase3.Stack"; + +component { + function init() { + stack = new Stack(); + } + + function doSomething() { + if (!stack.empty() ) { + // do stuff + } + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Chihuahua.cfc b/src/test/java/TestCases/asm/phase3/Chihuahua.cfc new file mode 100644 index 000000000..f1619b84d --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Chihuahua.cfc @@ -0,0 +1,17 @@ +component extends="Dog" { + results.append('Chihuahua pseudo ' & getFileFromPath( getCurrentTemplatePath())) + + function init() { + super.init(); + results.append('Chihuahua init ' & getFileFromPath( getCurrentTemplatePath())) + } + + function speak() { + return "Yip Yip!"; + } + + function getScientificName() { + return "barkus annoyus " & super.getScientificName(); + } + + } \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Child.cfc b/src/test/java/TestCases/asm/phase3/Child.cfc new file mode 100644 index 000000000..ffc8ed46e --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Child.cfc @@ -0,0 +1,12 @@ +component extends="Parent" { + + function init() { + return super.init(); + } + + private void function setupFrameworkDefaults() { + request.calls.append( "running child setupFrameworkDefaults()" ); + super.setupFrameworkDefaults(); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ClassLeadingComment.cfc b/src/test/java/TestCases/asm/phase3/ClassLeadingComment.cfc new file mode 100644 index 000000000..e624afcaf --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ClassLeadingComment.cfc @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ClassTest.java b/src/test/java/TestCases/asm/phase3/ClassTest.java new file mode 100644 index 000000000..304ebb4b6 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ClassTest.java @@ -0,0 +1,1264 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package TestCases.asm.phase3; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +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; + +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.interop.DynamicObject; +import ortus.boxlang.runtime.runnables.IClassRunnable; +import ortus.boxlang.runtime.runnables.RunnableLoader; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.RequestScope; +import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.Array; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.Struct; +import ortus.boxlang.runtime.types.exceptions.AbstractClassException; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.types.meta.ClassMeta; + +public class ClassTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + static Key foo = new Key( "foo" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + instance.useASMBoxPiler(); + } + + @AfterEach + public void teardownEach() { + instance.useJavaBoxpiler(); + } + + @DisplayName( "Test can create vanilla module config" ) + @Test + void testVanillaModuleConfig() { + + IClassRunnable cfc = ( IClassRunnable ) DynamicObject.of( RunnableLoader.getInstance().loadClass( + """ + /** + * This is the module descriptor and entry point for your module in the Runtime. + * The unique name of the moduel is the name of the directory on the modules folder. + * A BoxLang Mapping will be created for you with the name of the module. + * + * A Module can have the following folders that will be automatically registered: + * + bifs - Custom BIFs that will be registered into the runtime + * + interceptors - Custom Interceptors that will be registered into the runtime via the configure() method + * + libs - Custom Java libraries that your module leverages + * + tags - Custom tags that will be registered into the runtime + * + * Every Module will have it's own ClassLoader that will be used to load the module libs and dependencies. + */ + component{ + + /** + * -------------------------------------------------------------------------- + * Module Properties + * -------------------------------------------------------------------------- + * Here is where you define the properties of your module that the module service + * will use to register and activate your module + */ + + /** + * Your module version. Try to use semantic versioning + * @mandatory + */ + this.version = "1.0.0"; + + /** + * The BoxLang mapping for your module. All BoxLang modules are registered with an internal + * mapping prefix of : bxModules.{this.mapping}, /bxmodules/{this.mapping}. Ex: bxModules.test, /bxmodules/test + */ + this.mapping = "test"; + + /** + * Who built the module + */ + this.author = "Luis Majano"; + + /** + * The module description + */ + this.description = "This module does amazing things"; + + /** + * The module web URL + */ + this.webURL = "https://www.ortussolutions.com"; + + /** + * This boolean flag tells the module service to skip the module registration/activation process. + */ + this.disabled = false; + + /** + * -------------------------------------------------------------------------- + * Module Methods + * -------------------------------------------------------------------------- + */ + + /** + * Called by the ModuleService on module registration + * + * @moduleRecord - The module record registered in the ModuleService + * @runtime - The Runtime instance + */ + function configure( moduleRecord, runtime ){ + /** + * Every module has a settings configuration object + */ + settings = { + loadedOn : now(), + loadedBy : "Luis Majano" + }; + + /** + * The module interceptors to register into the runtime + */ + interceptors = [ + // { class="path.to.Interceptor", properties={} } + { class="bxModules.test.interceptors.Listener", properties={} } + ]; + + /** + * A list of custom interception points to register into the runtime + */ + customInterceptionPoints = [ "onBxTestModule" ]; + } + + /** + * Called by the ModuleService on module activation + * + * @moduleRecord - The module record registered in the ModuleService + * @runtime - The Runtime instance + */ + function onLoad( moduleRecord, runtime ){ + + } + + /** + * Called by the ModuleService on module deactivation + * + * @moduleRecord - The module record registered in the ModuleService + * @runtime - The Runtime instance + */ + function onUnload( moduleRecord, runtime ){ + + } + + /** + * -------------------------------------------------------------------------- + * Module Events + * -------------------------------------------------------------------------- + * You can listen to any Runtime events by creating the methods + * that match the approved Runtime Interception Points + */ + } + + """, context, BoxSourceType.CFSCRIPT ) ) + .invokeConstructor( context ) + .getTargetInstance(); + } + + @DisplayName( "Test a basic boxlang class" ) + @Test + public void testBasicBLClass() { + + // @formatter:off + IClassRunnable bxClass = ( IClassRunnable ) DynamicObject.of( RunnableLoader.getInstance().loadClass( + """ + import foo; + import java.lang.System; + + /** + * This is my class description + * + * @brad wood + * @luis + */ + @foo "bar" + class accessors=true singleton gavin="pickin" inject { + + property numeric age default=1; + property numeric test; + property testAlone; + + variables.setup=true; + System.out.println( "word" ); + request.foo="bar"; + println( request.asString()) + isInitted = false; + println( "current template is " & getCurrentTemplatePath() ); + printLn( foo() ) + + function init() { + isInitted = true; + } + + function foo() { + return "I work! #bar()# #variables.setup# #setup# #request.foo# #isInitted#"; + } + + private function bar() { + return "whee"; + } + + function getThis() { + return this; + } + + function runThisFoo() { + return this.foo(); + } + } + """, context, BoxSourceType.BOXSCRIPT ) ) + .invokeConstructor( context ) + .getTargetInstance(); + // @formatter:on + + // Test shorthand properties work + assertThat( + bxClass.dereferenceAndInvoke( context, Key.of( "getAge" ), new Object[] {}, false ) + ).isEqualTo( 1 ); + assertThat( + bxClass.dereferenceAndInvoke( context, Key.of( "getTest" ), new Object[] {}, false ) + ).isEqualTo( null ); + assertThat( + bxClass.dereferenceAndInvoke( context, Key.of( "getTestAlone" ), new Object[] {}, false ) + ).isEqualTo( null ); + var mdProperties = bxClass.getMetaData().get( Key.of( "properties" ) ); + + // execute public method + Object funcResult = bxClass.dereferenceAndInvoke( context, Key.of( "foo" ), new Object[] {}, false ); + + // private methods error + Throwable t = assertThrows( BoxRuntimeException.class, + () -> bxClass.dereferenceAndInvoke( context, Key.of( "bar" ), new Object[] {}, false ) ); + assertThat( t.getMessage().contains( "bar" ) ).isTrue(); + + // Can call public method that accesses private method, and variables, and request scope + assertThat( funcResult ).isEqualTo( "I work! whee true true bar true" ); + assertThat( context.getScope( RequestScope.name ).get( Key.of( "foo" ) ) ).isEqualTo( "bar" ); + + // This scope is reference to actual CFC instance + funcResult = bxClass.dereferenceAndInvoke( context, Key.of( "getThis" ), new Object[] {}, false ); + assertThat( funcResult ).isEqualTo( bxClass ); + + // Can call public methods on this + funcResult = bxClass.dereferenceAndInvoke( context, Key.of( "runThisFoo" ), new Object[] {}, false ); + assertThat( funcResult ).isEqualTo( "I work! whee true true bar true" ); + } + + @DisplayName( "basic class" ) + @Test + public void testBasicCFClass() { + // @formatter:off + IClassRunnable cfc = ( IClassRunnable ) DynamicObject.of( RunnableLoader.getInstance().loadClass( + """ + /** + * This is my class description + * + * @brad wood + * @luis + */ + component singleton gavin="pickin" inject foo="bar" { + + variables.setup=true; + createObject('java','java.lang.System').out.println( "word" ); + request.foo="bar"; + println( request.asString()) + isInitted = false; + println( "current template is " & getCurrentTemplatePath() ); + printLn( foo() ) + + function init() { + isInitted = true; + } + + function foo() { + return "I work! #bar()# #variables.setup# #setup# #request.foo# #isInitted#"; + } + + private function bar() { + return "whee" ; + } + + function getThis() { + return this; + } + + function runThisFoo() { + return this.foo() ; + } + } + + + """, context, BoxSourceType.CFSCRIPT ) ) + .invokeConstructor( context ) + .getTargetInstance(); + // @formatter:on + + // execute public method + Object funcResult = cfc.dereferenceAndInvoke( context, Key.of( "foo" ), new Object[] {}, false ); + + // private methods error + Throwable t = assertThrows( BoxRuntimeException.class, + () -> cfc.dereferenceAndInvoke( context, Key.of( "bar" ), new Object[] {}, false ) ); + assertThat( t.getMessage().contains( "bar" ) ).isTrue(); + + // Can call public method that accesses private method, and variables, and request scope + assertThat( funcResult ).isEqualTo( "I work! whee true true bar true" ); + assertThat( context.getScope( RequestScope.name ).get( Key.of( "foo" ) ) ).isEqualTo( "bar" ); + + // This scope is reference to actual CFC instance + funcResult = cfc.dereferenceAndInvoke( context, Key.of( "getThis" ), new Object[] {}, false ); + assertThat( funcResult ).isEqualTo( cfc ); + + // Can call public methods on this + funcResult = cfc.dereferenceAndInvoke( context, Key.of( "runThisFoo" ), new Object[] {}, false ); + assertThat( funcResult ).isEqualTo( "I work! whee true true bar true" ); + } + + @DisplayName( "basic class file" ) + @Test + public void testBasicClassFile() { + // @formatter:off + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.MyClass(); + // execute public method + result = cfc.foo(); + + // private methods error + try { + cfc.bar() + assert false; + } catch( BoxRuntimeException e ) { + assert e.message contains "bar"; + } + + // Can call public method that accesses private method, and variables, and request scope + assert result == "I work! whee true true bar true"; + assert request.foo == "bar"; + + // This scope is reference to actual CFC instance + assert cfc.$bx.$class.getName() == cfc.getThis().$bx.$class.getName(); + + // Can call public methods on this + assert cfc.runThisFoo() == "I work! whee true true bar true"; + """, context ); + // @formatter:on + } + + @DisplayName( "legacy meta" ) + @Test + public void testlegacyMeta() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.MyClass(); + """, context ); + + var cfc = variables.getAsClassRunnable( Key.of( "cfc" ) ); + var meta = cfc.getMetaData(); + assertThat( meta.get( Key.of( "name" ) ) ).isEqualTo( "src.test.java.TestCases.phase3.MyClass" ); + assertThat( meta.get( Key.of( "type" ) ) ).isEqualTo( "Component" ); + assertThat( meta.get( Key.of( "fullname" ) ) ).isEqualTo( "src.test.java.TestCases.phase3.MyClass" ); + assertThat( meta.getAsString( Key.of( "path" ) ).contains( "MyClass.bx" ) ).isTrue(); + // assertThat( meta.get( Key.of( "hashcode" ) ) ).isEqualTo( cfc.hashCode() ); + assertThat( meta.get( Key.of( "properties" ) ) ).isInstanceOf( Array.class ); + assertThat( meta.getAsArray( Key.of( "properties" ) ) ).hasSize( 1 ); + Struct prop = ( Struct ) meta.getAsArray( Key.of( "properties" ) ).get( 0 ); + assertThat( prop ).doesNotContainKey( Key.of( "defaultValue" ) ); + + assertThat( meta.get( Key.of( "functions" ) ) instanceof Array ).isTrue(); + assertThat( meta.getAsArray( Key.of( "functions" ) ).size() ).isEqualTo( 5 ); + assertThat( meta.get( Key.of( "extends" ) ) ).isNull(); + assertThat( meta.get( Key.of( "output" ) ) ).isEqualTo( false ); + assertThat( meta.get( Key.of( "persisent" ) ) ).isEqualTo( false ); + assertThat( meta.get( Key.of( "accessors" ) ) ).isEqualTo( true ); + } + + @DisplayName( "legacy meta CF" ) + @Test + public void testlegacyMetaCF() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.MyClassCF(); + """, context ); + + var cfc = variables.getAsClassRunnable( Key.of( "cfc" ) ); + var meta = cfc.getMetaData(); + assertThat( meta.get( Key.of( "name" ) ) ).isEqualTo( "src.test.java.TestCases.phase3.MyClassCF" ); + assertThat( meta.get( Key.of( "type" ) ) ).isEqualTo( "Component" ); + assertThat( meta.get( Key.of( "fullname" ) ) ).isEqualTo( "src.test.java.TestCases.phase3.MyClassCF" ); + assertThat( meta.getAsString( Key.of( "path" ) ).contains( "MyClassCF.cfc" ) ).isTrue(); + // assertThat( meta.get( Key.of( "hashcode" ) ) ).isEqualTo( cfc.hashCode() ); + assertThat( meta.get( Key.of( "properties" ) ) ).isInstanceOf( Array.class ); + assertThat( meta.get( Key.of( "functions" ) ) instanceof Array ).isTrue(); + assertThat( meta.getAsArray( Key.of( "functions" ) ).size() ).isEqualTo( 5 ); + assertThat( meta.get( Key.of( "extends" ) ) ).isNull(); + assertThat( meta.get( Key.of( "output" ) ) ).isEqualTo( true ); + assertThat( meta.get( Key.of( "persisent" ) ) ).isEqualTo( false ); + assertThat( meta.get( Key.of( "accessors" ) ) ).isEqualTo( false ); + } + + @DisplayName( "It should call onMissingMethod with pos args" ) + @Test + public void testOnMissingMethodPos() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.OnMissingMethod(); + result = cfc.someFunc( "first", "second" ); + """, context ); + + String res = variables.getAsString( result ); + assertThat( res ).isEqualTo( "someFuncsecond" ); + } + + @DisplayName( "It should call onMissingMethod with named args" ) + @Test + public void testOnMissingMethodNamed() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.OnMissingMethod(); + result = cfc.someFunc( foo="first", bar="second" ); + """, context ); + + String res = variables.getAsString( result ); + assertThat( res ).isEqualTo( "someFuncsecond" ); + } + + @DisplayName( "box meta" ) + @Test + public void testBoxMeta() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.MyClass(); + """, context ); + + var cfc = variables.getAsClassRunnable( Key.of( "cfc" ) ); + var boxMeta = ( ClassMeta ) cfc.getBoxMeta(); + var meta = boxMeta.meta; + assertThat( meta.get( Key.of( "type" ) ) ).isEqualTo( "Component" ); + assertThat( meta.get( Key.of( "fullname" ) ) ).isEqualTo( "src.test.java.TestCases.phase3.MyClass" ); + assertThat( meta.getAsString( Key.of( "path" ) ).contains( "MyClass.bx" ) ).isTrue(); + assertThat( meta.get( Key.of( "hashcode" ) ) ).isEqualTo( cfc.hashCode() ); + assertThat( meta.get( Key.of( "properties" ) ) instanceof Array ).isTrue(); + assertThat( meta.get( Key.of( "functions" ) ) instanceof Array ).isTrue(); + + assertThat( meta.get( Key.of( "extends" ) ) instanceof IStruct ).isTrue(); + + assertThat( meta.getAsArray( Key.of( "functions" ) ).size() ).isEqualTo( 5 ); + var fun1 = meta.getAsArray( Key.of( "functions" ) ).get( 0 ); + assertThat( fun1 ).isInstanceOf( Struct.class ); + assertThat( ( ( IStruct ) fun1 ).containsKey( Key.of( "name" ) ) ).isTrue(); + System.out.println( meta.getAsArray( Key.of( "functions" ) ).asString() ); + + assertThat( meta.get( Key.of( "documentation" ) ) instanceof IStruct ).isTrue(); + var docs = meta.getAsStruct( Key.of( "documentation" ) ); + assertThat( docs.getAsString( Key.of( "brad" ) ).trim() ).isEqualTo( "wood" ); + assertThat( docs.get( Key.of( "luis" ) ) ).isEqualTo( "" ); + assertThat( docs.getAsString( Key.of( "hint" ) ).trim() ).isEqualTo( "This is my class description continued on this line \nand this one as well." ); + + assertThat( meta.get( Key.of( "annotations" ) ) instanceof IStruct ).isTrue(); + var annos = meta.getAsStruct( Key.of( "annotations" ) ); + assertThat( annos.getAsString( Key.of( "foo" ) ).trim() ).isEqualTo( "bar" ); + // assertThat( annos.getAsString( Key.of( "implements" ) ).trim() ).isEqualTo( "Luis,Jorge" ); + assertThat( annos.getAsString( Key.of( "singleton" ) ).trim() ).isEqualTo( "" ); + assertThat( annos.getAsString( Key.of( "gavin" ) ).trim() ).isEqualTo( "pickin" ); + assertThat( annos.getAsString( Key.of( "inject" ) ).trim() ).isEqualTo( "" ); + + } + + @DisplayName( "properties" ) + @Test + public void testProperties() { + // @formatter:off + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.PropertyTest(); + nameGet = cfc.getMyProperty(); + invalidSetErrored=false; + try { + // property is typed as string, an array should blow up + setResult = cfc.setMyProperty( [] ); + } catch( any e ) { + invalidSetErrored=true; + } + setResult = cfc.setMyProperty( "anotherValue" ); + nameGet2 = cfc.getMyProperty(); + test1 = cfc.getShortcutWithDefault() + test2 = cfc.getTypedShortcutWithDefault() + """, context ); + // @formatter:on + + var cfc = variables.getAsClassRunnable( Key.of( "cfc" ) ); + assertThat( variables.get( Key.of( "nameGet" ) ) ).isEqualTo( "myDefaultValue" ); + assertThat( variables.get( Key.of( "nameGet2" ) ) ).isEqualTo( "anotherValue" ); + assertThat( variables.get( Key.of( "setResult" ) ) ).isEqualTo( cfc ); + assertThat( variables.get( Key.of( "invalidSetErrored" ) ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "test1" ) ) ).isEqualTo( "myDefaultValue" ); + assertThat( variables.get( Key.of( "test2" ) ) ).isEqualTo( "myDefaultValue2" ); + + var boxMeta = ( ClassMeta ) cfc.getBoxMeta(); + var meta = boxMeta.meta; + + assertThat( meta.getAsArray( Key.of( "properties" ) ).size() ).isEqualTo( 7 ); + + var prop1 = ( IStruct ) meta.getAsArray( Key.of( "properties" ) ).get( 0 ); + assertThat( prop1.get( "name" ) ).isEqualTo( "myProperty" ); + assertThat( prop1.get( "defaultValue" ) ).isEqualTo( "myDefaultValue" ); + assertThat( prop1.get( "type" ) ).isEqualTo( "string" ); + + var prop1Annotations = prop1.getAsStruct( Key.of( "annotations" ) ); + assertThat( prop1Annotations.size() ).isEqualTo( 5 ); + + assertThat( prop1Annotations.containsKey( Key.of( "preAnno" ) ) ).isTrue(); + assertThat( prop1Annotations.get( Key.of( "preAnno" ) ) ).isEqualTo( "" ); + + assertThat( prop1Annotations.containsKey( Key.of( "inject" ) ) ).isTrue(); + assertThat( prop1Annotations.get( Key.of( "inject" ) ) ).isEqualTo( "" ); + + var prop2 = ( IStruct ) meta.getAsArray( Key.of( "properties" ) ).get( 1 ); + assertThat( prop2.get( "name" ) ).isEqualTo( "anotherprop" ); + assertThat( prop2.get( "defaultValue" ) ).isEqualTo( null ); + assertThat( prop2.get( "type" ) ).isEqualTo( "string" ); + + var prop2Annotations = prop2.getAsStruct( Key.of( "annotations" ) ); + assertThat( prop2Annotations.size() ).isEqualTo( 4 ); + + assertThat( prop2Annotations.containsKey( Key.of( "preAnno" ) ) ).isTrue(); + assertThat( prop2Annotations.get( Key.of( "preAnno" ) ) instanceof Array ).isTrue(); + Array preAnno = prop2Annotations.getAsArray( Key.of( "preAnno" ) ); + assertThat( preAnno.size() ).isEqualTo( 2 ); + assertThat( preAnno.get( 0 ) ).isEqualTo( "myValue" ); + assertThat( preAnno.get( 1 ) ).isEqualTo( "anothervalue" ); + + var prop3 = ( IStruct ) meta.getAsArray( Key.of( "properties" ) ).get( 2 ); + assertThat( prop3.get( "name" ) ).isEqualTo( "theName" ); + assertThat( prop3.get( "defaultValue" ) ).isEqualTo( null ); + assertThat( prop3.get( "type" ) ).isEqualTo( "any" ); + + var prop3Annotations = prop3.getAsStruct( Key.of( "annotations" ) ); + assertThat( prop3Annotations.size() ).isEqualTo( 4 ); + assertThat( prop3Annotations.containsKey( Key.of( "ID" ) ) ).isTrue(); + assertThat( prop3Annotations.get( Key.of( "ID" ) ) ).isEqualTo( "" ); + + var prop4 = ( IStruct ) meta.getAsArray( Key.of( "properties" ) ).get( 3 ); + assertThat( prop4.get( "name" ) ).isEqualTo( "name" ); + assertThat( prop4.get( "defaultValue" ) ).isEqualTo( null ); + assertThat( prop4.get( "type" ) ).isEqualTo( "string" ); + + var prop2Docs = prop2.getAsStruct( Key.of( "documentation" ) ); + assertThat( prop2Docs.size() ).isEqualTo( 3 ); + assertThat( prop2Docs.getAsString( Key.of( "brad" ) ).trim() ).isEqualTo( "wood" ); + assertThat( prop2Docs.getAsString( Key.of( "luis" ) ).trim() ).isEqualTo( "" ); + assertThat( prop2Docs.getAsString( Key.of( "hint" ) ).trim() ).isEqualTo( "This is my property" ); + } + + @DisplayName( "properties" ) + @Test + public void testPropertiesCF() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.PropertyTestCF(); + nameGet = cfc.getMyProperty(); + setResult = cfc.SetMyProperty( "anotherValue" ); + nameGet2 = cfc.getMyProperty(); + test1 = cfc.getShortcutWithDefault() + test2 = cfc.getTypedShortcutWithDefault() + """, context ); + + var cfc = variables.getAsClassRunnable( Key.of( "cfc" ) ); + + assertThat( variables.get( Key.of( "nameGet" ) ) ).isEqualTo( "myDefaultValue" ); + assertThat( variables.get( Key.of( "nameGet2" ) ) ).isEqualTo( "anotherValue" ); + assertThat( variables.get( Key.of( "setResult" ) ) ).isEqualTo( cfc ); + assertThat( variables.get( Key.of( "test1" ) ) ).isEqualTo( "myDefaultValue" ); + assertThat( variables.get( Key.of( "test2" ) ) ).isEqualTo( "myDefaultValue2" ); + + var boxMeta = ( ClassMeta ) cfc.getBoxMeta(); + var meta = boxMeta.meta; + + assertThat( meta.getAsArray( Key.of( "properties" ) ).size() ).isEqualTo( 5 ); + + var prop1 = ( IStruct ) meta.getAsArray( Key.of( "properties" ) ).get( 0 ); + assertThat( prop1.get( "name" ) ).isEqualTo( "myProperty" ); + assertThat( prop1.get( "defaultValue" ) ).isEqualTo( "myDefaultValue" ); + assertThat( prop1.get( "type" ) ).isEqualTo( "string" ); + + var prop1Annotations = prop1.getAsStruct( Key.of( "annotations" ) ); + assertThat( prop1Annotations.size() ).isEqualTo( 5 ); + + assertThat( prop1Annotations.containsKey( Key.of( "preAnno" ) ) ).isTrue(); + assertThat( prop1Annotations.get( Key.of( "preAnno" ) ) ).isEqualTo( "" ); + + assertThat( prop1Annotations.containsKey( Key.of( "inject" ) ) ).isTrue(); + assertThat( prop1Annotations.get( Key.of( "inject" ) ) ).isEqualTo( "" ); + + var prop2 = ( IStruct ) meta.getAsArray( Key.of( "properties" ) ).get( 1 ); + assertThat( prop2.get( "name" ) ).isEqualTo( "anotherprop" ); + assertThat( prop2.get( "defaultValue" ) ).isEqualTo( null ); + assertThat( prop2.get( "type" ) ).isEqualTo( "string" ); + + var prop2Annotations = prop2.getAsStruct( Key.of( "annotations" ) ); + assertThat( prop2Annotations.size() ).isEqualTo( 6 ); + + assertThat( prop2Annotations.containsKey( Key.of( "preAnno" ) ) ).isTrue(); + assertThat( prop2Annotations.get( Key.of( "preAnno" ) ) instanceof Array ).isTrue(); + Array preAnno = prop2Annotations.getAsArray( Key.of( "preAnno" ) ); + assertThat( preAnno.size() ).isEqualTo( 2 ); + assertThat( preAnno.get( 0 ) ).isEqualTo( "myValue" ); + assertThat( preAnno.get( 1 ) ).isEqualTo( "anothervalue" ); + + var prop2Docs = prop2.getAsStruct( Key.of( "documentation" ) ); + assertThat( prop2Docs.size() ).isEqualTo( 3 ); + assertThat( prop2Docs.getAsString( Key.of( "brad" ) ).trim() ).isEqualTo( "wood" ); + assertThat( prop2Docs.getAsString( Key.of( "luis" ) ).trim() ).isEqualTo( "" ); + assertThat( prop2Docs.getAsString( Key.of( "hint" ) ).trim() ).isEqualTo( "This is my property" ); + + } + + @DisplayName( "Implicit Constructor named" ) + @Test + public void testImplicitConstructorNamed() { + + instance.executeSource( + """ + 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 named argumentCollection" ) + @Test + public void testImplicitConstructorNamedArgumentCollection() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.ImplicitConstructorTest( argumentCollection={ 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.executeSource( + """ + 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.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.InitMethodTest( ); + + result = cfc.getInittedProperly(); + """, context ); + + assertThat( variables.get( Key.of( "result" ) ) ).isEqualTo( true ); + + } + + @DisplayName( "PseudoConstructor can output" ) + @Test + public void testPseudoConstructorOutput() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.PseudoConstructorOutput(); + result = getBoxContext().getBuffer().toString() + + """, context ); + + assertThat( variables.get( Key.of( "result" ) ) ).isEqualTo( "PseudoConstructorOutput" ); + + } + + @DisplayName( "PseudoConstructor will not output" ) + @Test + public void testPseudoConstructorNoOutput() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.PseudoConstructorNoOutput(); + result = getBoxContext().getBuffer().toString() + + """, context ); + + assertThat( variables.get( Key.of( "result" ) ) ).isEqualTo( "" ); + + } + + @DisplayName( "can extend" ) + @Test + public void testCanExtend() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.Chihuahua(); + result = cfc.speak() + warm = cfc.isWarmBlooded() + name = cfc.getScientificName() + results = cfc.getResults() + """, context ); + + // Polymorphism invokes overridden method + assertThat( variables.get( Key.of( "result" ) ) ).isEqualTo( "Yip Yip!" ); + // inherited method from base class + assertThat( variables.get( Key.of( "warm" ) ) ).isEqualTo( true ); + // Delegate to super.method() in parent class + assertThat( variables.get( Key.of( "name" ) ) ).isEqualTo( "barkus annoyus Canis lupus Animal Kingdom" ); + + // This array represents a specific order of operations that occur during the instantiation of our object hierachy + // as well as specific values that need to be present to ensure correct behaviors + assertThat( variables.getAsArray( Key.of( "results" ) ).toArray() ).isEqualTo( new Object[] { + // top most super class is instantiated first. getCurrentTemplate() shows that file + "animal pseudo Animal.cfc", + // Then the next super class is instantiated. getCurrentTemplate() shows that file + "Dog pseudo Dog.cfc", + // The variables scope in the Doc pseudo constructor is the "same" variables scope as the Animal pseudo constructor that ran before it + "dog sees variables.inAnimal as: true", + // And lastly, the concrete class is instantiated. getCurrentTemplate() shows that file + "Chihuahua pseudo Chihuahua.cfc", + // I'm calling super.init() first, so animal inits first. getCurrentTemplate() shows the current class. INCOMPAT WITH CF which returns concrete + // class! + "Animal init Animal.cfc", + // Then dog inits as we work backwards. getCurrentTemplate() shows the current class. INCOMPAT WITH CF which returns concrete class! + "Dog init Dog.cfc", + // Then the concrete class inits. getCurrentTemplate() shows the concrete class. + "Chihuahua init Chihuahua.cfc", + // A method inherited from a base class, sees "this" as the concrete class. + "animal this is: src.test.java.TestCases.phase3.Chihuahua", + // A method inherited from a base class, sees the top level "variables" scope. + "animal sees inDog as: true", + // A method delegated to as super.foo() sees "this" as the concrete class. + "super animal sees: src.test.java.TestCases.phase3.Chihuahua", + // A method delegated to as super.foo() sees the top level "variables" scope. + "super sees inDog as: true", + } ); + + var cfc = variables.getAsClassRunnable( Key.of( "cfc" ) ); + var boxMeta = ( ClassMeta ) cfc.getBoxMeta(); + var meta = boxMeta.meta; + + assertThat( meta.get( Key.of( "name" ) ) ).isEqualTo( "src.test.java.TestCases.phase3.Chihuahua" ); + + IStruct extendsMeta = meta.getAsStruct( Key.of( "extends" ) ); + assertThat( extendsMeta.getAsString( Key.of( "name" ) ).endsWith( ".Dog" ) ).isTrue(); + + extendsMeta = extendsMeta.getAsStruct( Key.of( "extends" ) ); + assertThat( extendsMeta.getAsString( Key.of( "name" ) ).endsWith( ".Animal" ) ).isTrue(); + + extendsMeta = extendsMeta.getAsStruct( Key.of( "extends" ) ); + assertThat( extendsMeta ).hasSize( 0 ); + + } + + @DisplayName( "class as struct" ) + @Test + public void testClassAsStruct() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.MyClass(); + result = isStruct( cfc ) + cfc.foo = "bar" + result2 = structGet( "cfc.foo") + keyArray = structKeyArray( cfc ) + + """, context ); + + assertThat( variables.get( result ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "bar" ); + assertThat( variables.get( Key.of( "keyArray" ) ) ).isInstanceOf( Array.class ); + + } + + @Test + void testJavaMeta() { + // @formatter:off + instance.executeSource( + """ + jClass = createObject( "java", "java.lang.System" ) + result = jClass.$bx.meta + clazz = jClass.$bx.$class + println( clazz ) + """, context ); + // @formatter:on + assertThat( variables.get( result ) ).isInstanceOf( IStruct.class ); + assertThat( variables.get( Key.of( "clazz" ) ) ).isEqualTo( System.class ); + } + + @Test + public void testFunctionMeta() { + // @formatter:off + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.FunctionMeta(); + result = cfc.$bx.meta + println( result ) + """, context ); + // @formatter:on + assertThat( variables.get( result ) ).isInstanceOf( IStruct.class ); + } + + @Test + public void testSuperHeadlessFunctionInvocationToChild() { + + instance.executeSource( + """ + request.calls = []; + cfc = new src.test.java.TestCases.phase3.Child(); + result = request.calls; + """, context ); + + assertThat( variables.getAsArray( result ) ).hasSize( 2 ); + assertThat( variables.getAsArray( result ).get( 0 ) ).isEqualTo( "running child setupFrameworkDefaults()" ); + assertThat( variables.getAsArray( result ).get( 1 ) ).isEqualTo( "running parent setupFrameworkDefaults()" ); + } + + @Test + public void testClassWrappedInScriptIsland() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.ClassWrappedInScript(); + """, context ); + + } + + @Test + public void testClassIgnoreLeadingComment() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.ClassLeadingComment(); + """, context ); + + } + + @Test + public void testClassIgnoreTrailingComment() { + + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.ClassTrailingComment(); + """, context ); + + } + + @Test + public void testCFImport() { + + instance.executeSource( + """ + foo = new src.test.java.TestCases.phase3.CFImportTest(); + foo.doSomething(); + """, context ); + + } + + @Test + public void testCFImport2() { + // This version quotes the class being imported + instance.executeSource( + """ + foo = new src.test.java.TestCases.phase3.CFImportTest2(); + foo.doSomething(); + """, context ); + + } + + @Test + public void testInlineJavaImplements() { + instance.executeSource( + """ + import java:java.lang.Thread; + jRunnable = new src.test.java.TestCases.phase3.JavaImplements(); + assert jRunnable instanceof "java.lang.Runnable" + jThread = new java:Thread( jRunnable ); + jThread.start(); + """, context ); + + } + + // disabling this as it causes a crazy concurrency issue with another test + @Test + @Disabled + public void testInlineJavaExtendsASM() { + instance.executeSource( + """ + import java.util.Timer; + myASMTask = new src.test.java.TestCases.asm.phase3.JavaExtendsAsm(); + assert myASMTask instanceof "java.util.TimerTask" + + jtimer = new Timer(); + jtimer.schedule(myASMTask, 1000); + myASMTask.cancel() + """, context ); + + } + + @Test + public void testInlineJavaExtendsField() { + instance.executeSource( + """ + myContext = new src.test.java.TestCases.phase3.JavaExtends2(); + assert myContext instanceof "ortus.boxlang.runtime.context.IBoxContext" + + println( myContext.getTemplatesYo() ) + """, context ); + + } + + @Test + public void testInlineJavaExtendsFieldPublic() { + instance.executeSource( + """ + myBIF = new src.test.java.TestCases.phase3.JavaExtends3(); + assert myBIF instanceof "ortus.boxlang.runtime.bifs.BIF" + println( myBIF.__isMemberExecution ) + println( myBIF.runtime ) + myBIF.printStuff() + """, context ); + + } + + @Test + public void testImplicitAccessor() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.ImplicitAccessor(); + clazz.name="brad"; + clazz.age=44; + name = clazz.name; + age = clazz.age; + methodsCalled = clazz.getMethodsCalled(); + """, context ); + assertThat( variables.get( Key.of( "name" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "age" ) ) ).isEqualTo( 44 ); + assertThat( variables.get( Key.of( "methodsCalled" ) ) ).isEqualTo( "setNamesetAgegetNamegetAge" ); + } + + @Test + public void testImplicitGeneratedAccessor() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.ImplicitGeneratedAccessor(); + clazz.name="brad"; + clazz.age=44; + name = clazz.name; + age = clazz.age; + // prove they're going in the variable scope, not this scope + keyExistsName = structKeyExists( clazz, "name") + keyExistsAge = structKeyExists( clazz, "age") + """, context ); + assertThat( variables.get( Key.of( "name" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "age" ) ) ).isEqualTo( 44 ); + assertThat( variables.get( Key.of( "keyExistsName" ) ) ).isEqualTo( false ); + assertThat( variables.get( Key.of( "keyExistsAge" ) ) ).isEqualTo( false ); + } + + @Test + public void testStaticInstance() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.StaticTestCF(); + result1 = clazz.foo; + result2 = clazz.myStaticFunc(); + result3 = clazz.myInstanceFunc(); + result4 = clazz.scoped; + result5 = clazz.unscoped; + result6 = clazz.again; + """, context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 42 ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static42" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( "instancestatic42" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + } + + @Test + public void testStaticStatic() { + instance.executeSource( """ + result1 = src.test.java.TestCases.phase3.StaticTest::foo; + result2 = src.test.java.TestCases.phase3.StaticTest::myStaticFunc(); + result4 = src.test.java.TestCases.phase3.StaticTest::scoped; + result5 = src.test.java.TestCases.phase3.StaticTest::unscoped; + result6 = src.test.java.TestCases.phase3.StaticTest::again; + myStaticUDF = src.test.java.TestCases.phase3.StaticTest::sayHello; + result7 = myStaticUDF(); + result8 = src.test.java.TestCases.phase3.StaticTest::123; + """, context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "Hello" ); + assertThat( variables.get( Key.of( "result8" ) ) ).isEqualTo( 456 ); + } + + @Test + public void testStaticInstanceCF() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.StaticTestCF(); + result1 = clazz.foo; + result2 = clazz.myStaticFunc(); + result3 = clazz.myInstanceFunc(); + result4 = clazz.scoped; + result5 = clazz.unscoped; + result6 = clazz.again; + """, context, BoxSourceType.CFSCRIPT ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 42 ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static42" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( "instancestatic42" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + } + + @Test + public void testStaticStaticCF() { + instance.executeSource( + """ + result1 = src.test.java.TestCases.phase3.StaticTest::foo; + result2 = src.test.java.TestCases.phase3.StaticTest::myStaticFunc(); + result4 = src.test.java.TestCases.phase3.StaticTest::scoped; + result5 = src.test.java.TestCases.phase3.StaticTest::unscoped; + result6 = src.test.java.TestCases.phase3.StaticTest::again; + result7 = src.test.java.TestCases.phase3.StaticTest::123; + """, context, BoxSourceType.CFSCRIPT ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( 456 ); + } + + @Test + public void testStaticImport() { + instance.executeSource( + """ + import src.test.java.TestCases.phase3.StaticTest; + + result1 = StaticTest::foo; + result2 = StaticTest::myStaticFunc(); + result4 = StaticTest::scoped; + result5 = StaticTest::unscoped; + result6 = StaticTest::again; + """, context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + } + + @Test + public void testStaticImportDot() { + instance.executeSource( + """ + import src.test.java.TestCases.phase3.StaticTest; + + result1 = StaticTest.foo; + result2 = StaticTest.myStaticFunc(); + result4 = StaticTest.scoped; + result5 = StaticTest.unscoped; + result6 = StaticTest.again; + // instance + myInstance = new StaticTest(); + result7 = myInstance.foo; + result8 = StaticTest.foo; + result9 = myInstance.myInstanceFunc2() + """, context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( 42 ); + assertThat( variables.get( Key.of( "result8" ) ) ).isEqualTo( 42 ); + assertThat( variables.get( Key.of( "result9" ) ) ).isInstanceOf( Array.class ); + Array result9 = variables.getAsArray( Key.of( "result9" ) ); + assertThat( result9.size() ).isEqualTo( 3 ); + assertThat( result9.get( 0 ) ).isEqualTo( "brad" ); + assertThat( result9.get( 1 ) ).isEqualTo( "wood" ); + assertThat( result9.get( 2 ) ).isEqualTo( 42 ); + } + + @Test + public void testDotExtends() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.DotExtends(); + result = clazz.childUDF() + """, context ); + assertThat( variables.get( result ) ).isEqualTo( "childUDFparent" ); + } + + @Test + public void testRelativeInstantiation() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.RelativeInstantiation(); + result = clazz.findSibling() + """, context ); + assertThat( variables.get( result ) ).isEqualTo( "bar" ); + } + + @Test + public void testAbstractClass() { + Throwable t = assertThrows( AbstractClassException.class, () -> instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.AbstractClass(); + """, context ) ); + assertThat( t.getMessage() ).contains( "Cannot instantiate an abstract class" ); + + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.ConcreteClass(); + result1 = clazz.normal() + result2 = clazz.abstractMethod() + """, context ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "normal" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "abstractMethod" ); + } + + @Test + public void testAbstractClassCF() { + Throwable t = assertThrows( AbstractClassException.class, () -> instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.AbstractClassCF(); + """, context ) ); + assertThat( t.getMessage() ).contains( "Cannot instantiate an abstract class" ); + + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.ConcreteClassCF(); + result1 = clazz.normal() + result2 = clazz.abstractMethod() + """, context ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "normal" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "abstractMethod" ); + } + + @Test + public void testCFGetterType() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.GetterTest(); + result = clazz.getMyDate() + """, context ); + assertThat( variables.get( result ) ).isEqualTo( "" ); + } + + @Test + public void testGetterOverrideInParent() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.GeneratedGetterChild(); + result = clazz.getFoo() + """, context ); + assertThat( variables.get( result ) ).isEqualTo( "overriden" ); + } + + @Test + public void testFinalClass() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.FinalClass(); + """, context ); + Throwable t = assertThrows( BoxRuntimeException.class, + () -> instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.IllegalFinalExtends(); + """, context ) ); + assertThat( t.getMessage() ).contains( "Cannot extend final class" ); + } + +} diff --git a/src/test/java/TestCases/asm/phase3/ClassTrailingComment.cfc b/src/test/java/TestCases/asm/phase3/ClassTrailingComment.cfc new file mode 100644 index 000000000..a7606f5f0 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ClassTrailingComment.cfc @@ -0,0 +1,7 @@ + + + + + diff --git a/src/test/java/TestCases/asm/phase3/ClassWrappedInScript.cfc b/src/test/java/TestCases/asm/phase3/ClassWrappedInScript.cfc new file mode 100644 index 000000000..813fa9c83 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ClassWrappedInScript.cfc @@ -0,0 +1,4 @@ + + component { + } + \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ConcreteClass.bx b/src/test/java/TestCases/asm/phase3/ConcreteClass.bx new file mode 100644 index 000000000..da881c5b8 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ConcreteClass.bx @@ -0,0 +1,6 @@ +class extends="AbstractClass" { + + function abstractMethod() { + return "abstractMethod"; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ConcreteClassCF.cfc b/src/test/java/TestCases/asm/phase3/ConcreteClassCF.cfc new file mode 100644 index 000000000..06b946605 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ConcreteClassCF.cfc @@ -0,0 +1,6 @@ +component extends="AbstractClassCF" { + + function abstractMethod() { + return "abstractMethod"; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Dog.cfc b/src/test/java/TestCases/asm/phase3/Dog.cfc new file mode 100644 index 000000000..efdc62ab0 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Dog.cfc @@ -0,0 +1,20 @@ +component extends="Animal" { + results.append('Dog pseudo ' & getFileFromPath( getCurrentTemplatePath())) + variables.inDog = true; + // Our variables scope contains the variables from the parent component + results.append( "dog sees variables.inAnimal as: " & variables.inAnimal ) + + function init() { + super.init(); + results.append('Dog init ' & getFileFromPath( getCurrentTemplatePath())) + } + + function speak() { + return "Woof!"; + } + + function getScientificName() { + return "Canis lupus " & super.getScientificName(); + } + + } \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/DotExtends.cfc b/src/test/java/TestCases/asm/phase3/DotExtends.cfc new file mode 100644 index 000000000..28111cff1 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/DotExtends.cfc @@ -0,0 +1,5 @@ +component extends="../../TestCases/phase3/DotExtendsParent" { + function childUDF() { + return "childUDF" & super.parentUDF(); + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/DotExtendsParent.cfc b/src/test/java/TestCases/asm/phase3/DotExtendsParent.cfc new file mode 100644 index 000000000..55b42456b --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/DotExtendsParent.cfc @@ -0,0 +1,5 @@ +component { + function parentUDF() { + return "parent"; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ExceptionTest.java b/src/test/java/TestCases/asm/phase3/ExceptionTest.java new file mode 100644 index 000000000..c53f033e2 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ExceptionTest.java @@ -0,0 +1,77 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package TestCases.asm.phase3; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.VariablesScope; + +public class ExceptionTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + static Key foo = new Key( "foo" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + instance.useASMBoxPiler(); + } + + @AfterEach + public void teardownEach() { + instance.useJavaBoxpiler(); + } + + @Test + public void testBoxMeta() { + + instance.executeStatement( + """ + include "src/test/java/TestCases/phase3/ExceptionThrower.cfs"; + """, context ); + + assertThat( "" ).isEqualTo( "" ); + + } + +} diff --git a/src/test/java/TestCases/asm/phase3/ExceptionThrower.cfs b/src/test/java/TestCases/asm/phase3/ExceptionThrower.cfs new file mode 100644 index 000000000..9bb637d16 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ExceptionThrower.cfs @@ -0,0 +1,6 @@ +try { + 1/0 +} catch( any e ) { + e.printStackTrace() + println( e.tagContext ) +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/FinalClass.bx b/src/test/java/TestCases/asm/phase3/FinalClass.bx new file mode 100644 index 000000000..dc3b92ed8 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/FinalClass.bx @@ -0,0 +1,5 @@ +final class { + + final this.CONSTANT = "constant"; + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/FindMe.bx b/src/test/java/TestCases/asm/phase3/FindMe.bx new file mode 100644 index 000000000..9394e50f5 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/FindMe.bx @@ -0,0 +1,5 @@ +class { + function foo() { + return "bar"; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/FunctionMeta.cfc b/src/test/java/TestCases/asm/phase3/FunctionMeta.cfc new file mode 100644 index 000000000..1376b2f56 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/FunctionMeta.cfc @@ -0,0 +1,16 @@ +component { + + /** + * The bit that can be used to set all tasks created by this scheduler to always run on one server + */ + property name="serverFixation" type="boolean"; + + /** + * This is my function hint + * @brad wood + * @param1.luis majano + */ + function foo( param1 ) { + + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/GeneratedGetterChild.bx b/src/test/java/TestCases/asm/phase3/GeneratedGetterChild.bx new file mode 100644 index 000000000..015a3cdaa --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/GeneratedGetterChild.bx @@ -0,0 +1,2 @@ +class accessors=true extends="GeneratedGetterParent" { +} diff --git a/src/test/java/TestCases/asm/phase3/GeneratedGetterParent.bx b/src/test/java/TestCases/asm/phase3/GeneratedGetterParent.bx new file mode 100644 index 000000000..80852e652 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/GeneratedGetterParent.bx @@ -0,0 +1,9 @@ +class accessors=true { + + property name="foo" type=string default="default value"; + + // overridden getter + string function getFoo() { + return "overriden"; + } +} diff --git a/src/test/java/TestCases/asm/phase3/GetterTest.cfc b/src/test/java/TestCases/asm/phase3/GetterTest.cfc new file mode 100644 index 000000000..b85cf9750 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/GetterTest.cfc @@ -0,0 +1,10 @@ +component accessors="true" { + + property name="myDate" type="date"; + + function init() { + variables.myDate = ""; + } + + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IBicycle.bx b/src/test/java/TestCases/asm/phase3/IBicycle.bx new file mode 100644 index 000000000..a67cdd373 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IBicycle.bx @@ -0,0 +1,11 @@ +interface { + + public String function pedal( required numeric speed=3 ); + + function pedalLax( speed ); + + default function hasInnerTube() { + return true; + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IChildInterface.bx b/src/test/java/TestCases/asm/phase3/IChildInterface.bx new file mode 100644 index 000000000..c746358a5 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IChildInterface.bx @@ -0,0 +1,9 @@ +interface extends="IParentInterface" { + default function childDefaultMethod() { + return "childDefaultMethod"; + } + default function parentOverrideMe() { + return "parentOverrideMeChild"; + } + function childMethod(); +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IMotorcycle.bx b/src/test/java/TestCases/asm/phase3/IMotorcycle.bx new file mode 100644 index 000000000..a3aa0c08d --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IMotorcycle.bx @@ -0,0 +1,9 @@ +interface { + + function shift( required numeric gear ); + + default function needsFuel() { + return false; + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IMultiChildInterface.bx b/src/test/java/TestCases/asm/phase3/IMultiChildInterface.bx new file mode 100644 index 000000000..d73790082 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IMultiChildInterface.bx @@ -0,0 +1,12 @@ +interface extends="IParentInterface,IUncleInterface" { + default function childDefaultMethod() { + return "childDefaultMethod"; + } + default function parentOverrideMe() { + return "parentOverrideMeChild"; + } + default function uncleOverrideMe() { + return "uncleOverrideMeChild"; + } + function childMethod(); +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IParentInterface.bx b/src/test/java/TestCases/asm/phase3/IParentInterface.bx new file mode 100644 index 000000000..4f3a973b8 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IParentInterface.bx @@ -0,0 +1,12 @@ +interface { + default function parentDefaultMethod() { + return "parentDefaultMethod"; + } + default function parentOverrideMe() { + return "parentOverrideMeParent"; + } + default function parentOverrideMe2() { + return "parentOverrideMeParent2"; + } + function parentMethod(); +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IUncleInterface.bx b/src/test/java/TestCases/asm/phase3/IUncleInterface.bx new file mode 100644 index 000000000..c05e1f421 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IUncleInterface.bx @@ -0,0 +1,12 @@ +interface { + default function uncleDefaultMethod() { + return "uncleDefaultMethod"; + } + default function uncleOverrideMe() { + return "uncleOverrideMeUncle"; + } + default function uncleOverrideMe2() { + return "uncleOverrideMeUncle2"; + } + function uncleMethod(); +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/IllegalFinalExtends.bx b/src/test/java/TestCases/asm/phase3/IllegalFinalExtends.bx new file mode 100644 index 000000000..1442a25b7 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/IllegalFinalExtends.bx @@ -0,0 +1,3 @@ +class extends=FinalClass { + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ImplicitAccessor.bx b/src/test/java/TestCases/asm/phase3/ImplicitAccessor.bx new file mode 100644 index 000000000..fc2af56a6 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ImplicitAccessor.bx @@ -0,0 +1,29 @@ +class invokeImplicitAccessor=true { + property string name; + property integer age; + + methodsCalled = ""; + function getName() { + methodsCalled &= "getName"; + return variables.name; + } + + function setName(string name) { + methodsCalled &= "setName"; + variables.name = name; + } + + function getAge() { + methodsCalled &= "getAge"; + return variables.age; + } + + function setAge(integer age) { + methodsCalled &= "setAge"; + variables.age = age; + } + + function getMethodsCalled() { + return methodsCalled; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ImplicitConstructorTest.cfc b/src/test/java/TestCases/asm/phase3/ImplicitConstructorTest.cfc new file mode 100644 index 000000000..4186eec1b --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ImplicitConstructorTest.cfc @@ -0,0 +1,6 @@ +component accessors=true { + property name="name"; + property name="age"; + property name="favoriteColor"; + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/ImplicitGeneratedAccessor.bx b/src/test/java/TestCases/asm/phase3/ImplicitGeneratedAccessor.bx new file mode 100644 index 000000000..9e6d5e274 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/ImplicitGeneratedAccessor.bx @@ -0,0 +1,5 @@ +class invokeImplicitAccessor=true accessors=true { + property string name; + property integer age; + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/InitMethodTest.bx b/src/test/java/TestCases/asm/phase3/InitMethodTest.bx new file mode 100644 index 000000000..60a656d12 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/InitMethodTest.bx @@ -0,0 +1,9 @@ +class initMethod=birth accessors=true { + property inittedProperly default=false; + function init() { + throw "you'd better not call me!"; + } + function birth() { + inittedProperly=true + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/InterfaceInheritenceTest.bx b/src/test/java/TestCases/asm/phase3/InterfaceInheritenceTest.bx new file mode 100644 index 000000000..1abcc38b4 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/InterfaceInheritenceTest.bx @@ -0,0 +1,11 @@ +class implements="IChildInterface" { + + public Any function childMethod() { + return "childMethod"; + } + + public Any function parentMethod() { + return "parentMethod"; + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/InterfaceMultiInheritenceTest.bx b/src/test/java/TestCases/asm/phase3/InterfaceMultiInheritenceTest.bx new file mode 100644 index 000000000..74c0dedf9 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/InterfaceMultiInheritenceTest.bx @@ -0,0 +1,23 @@ +class implements="IMultiChildInterface" { + + public Any function childMethod() { + return "childMethod"; + } + + public Any function parentMethod() { + return "parentMethod"; + } + + public Any function uncleMethod() { + return "uncleMethod"; + } + + default function parentOverrideMe2() { + return "parentOverrideMe2Class"; + } + + default function uncleOverrideMe2() { + return "uncleOverrideMe2Class"; + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/InterfaceStatic.bx b/src/test/java/TestCases/asm/phase3/InterfaceStatic.bx new file mode 100644 index 000000000..3c9e628e0 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/InterfaceStatic.bx @@ -0,0 +1,16 @@ +interface { + + static { + static.myVar = "brad"; + yourVar = "luis"; + } + + static function foo() { + return static.myVar; + } + + static function callStatic() { + return foo(); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/InterfaceTest.java b/src/test/java/TestCases/asm/phase3/InterfaceTest.java new file mode 100644 index 000000000..cc301d42a --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/InterfaceTest.java @@ -0,0 +1,284 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package TestCases.asm.phase3; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.interop.DynamicObject; +import ortus.boxlang.runtime.runnables.BoxInterface; +import ortus.boxlang.runtime.runnables.RunnableLoader; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.VariablesScope; + +public class InterfaceTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + static Key foo = new Key( "foo" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + instance.useASMBoxPiler(); + } + + @AfterEach + public void teardownEach() { + instance.useJavaBoxpiler(); + } + + @DisplayName( "basic CF interface" ) + @Test + public void testBasicCFInterface() { + + BoxInterface inter = ( BoxInterface ) DynamicObject.of( RunnableLoader.getInstance().loadClass( + """ + /** + * This is my interface description + * + * @brad wood + * @luis + */ + interface singleton gavin="pickin" inject foo="bar" { + + function init(); + + function foo(); + + private function bar(); + + default function myDefaultMethod() { + return this.foo(); + } + + } + + """, context, BoxSourceType.CFSCRIPT ), context ).unWrapBoxLangClass(); + + assertThat( inter.getMetaData().get( Key.of( "type" ) ) ).isEqualTo( "Interface" ); + assertThat( inter.getMetaData().getAsArray( Key.of( "functions" ) ) ).hasSize( 4 ); + + } + + @DisplayName( "basic interface file CF" ) + @Test + public void testBasicInterfaceFileCF() { + + instance.executeStatement( + """ + result = createObject( "component", "src.test.java.TestCases.phase3.MyInterfaceCF" ); + """, context ); + assertThat( variables.get( result ) ).isInstanceOf( BoxInterface.class ); + BoxInterface inter = variables.getAsBoxInterface( result ); + assertThat( inter.getMetaData().get( Key.of( "type" ) ) ).isEqualTo( "Interface" ); + assertThat( inter.getMetaData().getAsArray( Key.of( "functions" ) ) ).hasSize( 4 ); + + } + + @DisplayName( "basic interface file BL" ) + @Test + public void testBasicInterfaceFileBL() { + + instance.executeStatement( + """ + result = createObject( "component", "src.test.java.TestCases.phase3.MyInterfaceBL" ); + """, context ); + assertThat( variables.get( result ) ).isInstanceOf( BoxInterface.class ); + BoxInterface inter = variables.getAsBoxInterface( result ); + assertThat( inter.getMetaData().get( Key.of( "type" ) ) ).isEqualTo( "Interface" ); + assertThat( inter.getMetaData().getAsArray( Key.of( "functions" ) ) ).hasSize( 4 ); + + } + + @DisplayName( "basic interface file CF Tag" ) + @Test + public void testBasicClassFileCFTag() { + + instance.executeStatement( + """ + result = createObject( "component", "src.test.java.TestCases.phase3.MyInterfaceCFTag" ); + """, context ); + assertThat( variables.get( result ) ).isInstanceOf( BoxInterface.class ); + BoxInterface inter = variables.getAsBoxInterface( result ); + assertThat( inter.getMetaData().get( Key.of( "type" ) ) ).isEqualTo( "Interface" ); + assertThat( inter.getMetaData().getAsArray( Key.of( "functions" ) ) ).hasSize( 4 ); + + } + + @DisplayName( "moped example" ) + @Test + public void testMopedExample() { + + instance.executeStatement( + """ + boxClass = new src.test.java.TestCases.phase3.Moped(); + + // implemented method from IBicycle + result1 = boxClass.pedal( 3 ) + // impelemented method from IMotorcycle + result2 = boxClass.shift( 4 ) + // Default method from IBicycle + result3 = boxClass.hasInnerTube() + // Default method from IMotorcycle but overridden by moped + result4 = boxClass.needsFuel() + """, context ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "Pedal speed 3" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "Shift to gear 4" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( true ); + } + + @DisplayName( "onMissingMethod example" ) + @Test + public void testOnMissingMethod() { + + instance.executeStatement( + """ + boxClass = new src.test.java.TestCases.phase3.WheeledThing(); + + // implemented method from IBicycle + result1 = boxClass.pedal( 3 ) + // impelemented method from IMotorcycle + result2 = boxClass.shift( 4 ) + // Default method from IBicycle + result3 = boxClass.hasInnerTube() + // Default method from IMotorcycle but overridden by moped + result4 = boxClass.needsFuel() + """, context ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "Pedal speed 3" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "Shift to gear 4" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( false ); + + } + + @DisplayName( "interface inheritence example" ) + @Test + public void testInterfaceInheritence() { + + instance.executeStatement( + """ + boxClass = new src.test.java.TestCases.phase3.InterfaceInheritenceTest(); + result1 = boxClass.childMethod() + result2 = boxClass.parentMethod() + result3 = boxClass.childDefaultMethod() + result4 = boxClass.parentDefaultMethod() + result5 = boxClass.parentOverrideMe() + assert boxClass instanceof "InterfaceInheritenceTest"; + assert boxClass instanceof "IChildInterface"; + assert boxClass instanceof "IParentInterface"; + """, context ); + + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "childMethod" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "parentMethod" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( "childDefaultMethod" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "parentDefaultMethod" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "parentOverrideMeChild" ); + } + + @DisplayName( "interface multi-inheritence example" ) + @Test + public void testInterfaceMultiInheritence() { + + instance.executeStatement( + """ + boxClass = new src.test.java.TestCases.phase3.InterfaceMultiInheritenceTest(); + result1 = boxClass.childMethod() + result2 = boxClass.parentMethod() + result3 = boxClass.uncleMethod() + result4 = boxClass.childDefaultMethod() + result5 = boxClass.parentDefaultMethod() + result6 = boxClass.uncleDefaultMethod() + result7 = boxClass.parentOverrideMe() + result8 = boxClass.uncleOverrideMe() + result9 = boxClass.parentOverrideMe2() + result10 = boxClass.uncleOverrideMe2() + assert boxClass instanceof "InterfaceMultiInheritenceTest"; + assert boxClass instanceof "IMultiChildInterface"; + assert boxClass instanceof "IParentInterface"; + assert boxClass instanceof "IUncleInterface"; + // println( createOBject("src.test.java.TestCases.phase3.IMultiChildInterface" ).$bx.invokeTargetMethod( getBoxContext(), "getMetaData", [].toArray() ) ) + """, + context ); + + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "childMethod" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "parentMethod" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( "uncleMethod" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "childDefaultMethod" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "parentDefaultMethod" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "uncleDefaultMethod" ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "parentOverrideMeChild" ); + assertThat( variables.get( Key.of( "result8" ) ) ).isEqualTo( "uncleOverrideMeChild" ); + assertThat( variables.get( Key.of( "result9" ) ) ).isEqualTo( "parentOverrideMe2Class" ); + assertThat( variables.get( Key.of( "result10" ) ) ).isEqualTo( "uncleOverrideMe2Class" ); + + } + + @DisplayName( "interface inheritence static" ) + @Test + public void testInterfaceStatic() { + + instance.executeStatement( + """ + import src.test.java.TestCases.phase3.InterfaceStatic as ist; + result1 = ist.foo() + result2 = ist.myVar; + result3 = ist.yourVar; + result4 = ist.callStatic(); + result5 = ist::yourVar; + result6 = ist::callStatic(); + result7 = src.test.java.TestCases.phase3.InterfaceStatic::yourVar; + result8 = src.test.java.TestCases.phase3.InterfaceStatic::callStatic(); + """, context ); + assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "brad" ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result8" ) ) ).isEqualTo( "brad" ); + + } + +} diff --git a/src/test/java/TestCases/asm/phase3/JavaExtends2.bx b/src/test/java/TestCases/asm/phase3/JavaExtends2.bx new file mode 100644 index 000000000..673799e63 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/JavaExtends2.bx @@ -0,0 +1,7 @@ +class extends="java:ortus.boxlang.runtime.context.BaseBoxContext" { + + function getTemplatesYo() { + return super.templates; + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/JavaExtends3.bx b/src/test/java/TestCases/asm/phase3/JavaExtends3.bx new file mode 100644 index 000000000..2aef9391f --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/JavaExtends3.bx @@ -0,0 +1,14 @@ +class extends="java:ortus.boxlang.runtime.bifs.BIF" { + + @overrideJava + Object function _invoke(ortus.boxlang.runtime.context.IBoxContext c ,ortus.boxlang.runtime.scopes.ArgumentsScope args) { + + } + + function printStuff() { + println( this.__isMemberExecution ) + println( this.runtime ) + println( super.__isMemberExecution ) + println( super.runtime ) + } +} diff --git a/src/test/java/TestCases/asm/phase3/JavaExtendsAsm.bx b/src/test/java/TestCases/asm/phase3/JavaExtendsAsm.bx new file mode 100644 index 000000000..09ca312d3 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/JavaExtendsAsm.bx @@ -0,0 +1,16 @@ +class extends="java:java.util.TimerTask" { + + + + @overrideJava + void function run() { + println("Hello from a custom TimerTask!" ); + println( super.scheduledExecutionTime() ); + } + + @overrideJava + public long function scheduledExecutionTime() { + throw "no, not me!"; + } + +} diff --git a/src/test/java/TestCases/asm/phase3/JavaImplements.bx b/src/test/java/TestCases/asm/phase3/JavaImplements.bx new file mode 100644 index 000000000..ba69db7f7 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/JavaImplements.bx @@ -0,0 +1,7 @@ +class implements="java:java.lang.Runnable" { + + function run() { + println("Hello from a thread!"); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Moped.bx b/src/test/java/TestCases/asm/phase3/Moped.bx new file mode 100644 index 000000000..7816cd1f4 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Moped.bx @@ -0,0 +1,20 @@ +class implements="IBicycle,IMotorcycle" { + + String function pedal( required numeric speed=3 ) { + return "Pedal speed #speed#" + } + + // This overachieving mehod has more details than the interfaces requires + Boolean function pedalLax( required numeric speed=3 ) { + } + + public Any function shift(required numeric gear) { + return "Shift to gear #gear#" + } + + @Override + default function needsFuel() { + return true; + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/MyClass.bx b/src/test/java/TestCases/asm/phase3/MyClass.bx new file mode 100644 index 000000000..35a26f027 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/MyClass.bx @@ -0,0 +1,50 @@ +// get foo +import foo; +// get system +import java.lang.System; + +/** + * This is my class description + * continued on this line + * + * and this one + * as well. + * + * @brad wood + * @luis + */ +@foo "bar" +class singleton gavin="pickin" inject { + + property name; + + variables.setup=true; // ending comment + System.out.println( "word" ); + request.foo="bar"; + isInitted = false; + println( "current template is " & getCurrentTemplatePath() ); + printLn( foo() ) + + // my init func + function init() { + isInitted = true; + } + + function foo() { + /* return this value */ + return "I work! #bar()# #variables.setup# #setup# #request.foo# #isInitted#"; + } + + private function bar() { + return "whee"; + } + + function getThis() { + return this; + } + + function runThisFoo() { + return this.foo(); + } + + } \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/MyClassCF.cfc b/src/test/java/TestCases/asm/phase3/MyClassCF.cfc new file mode 100644 index 000000000..7aa68b7bb --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/MyClassCF.cfc @@ -0,0 +1,36 @@ +/** + * This is my class description + * + * @brad wood + * @luis + */ +component singleton gavin="pickin" inject foo="bar" { + + variables.setup=true; + createObject('java','java.lang.System').out.println( "word" ); + request.foo="bar"; + isInitted = false; + println( "current template is " & getCurrentTemplatePath() ); + printLn( foo() ) + + function init() { + isInitted = true; + } + + function foo() { + return "I work! #bar()# #variables.setup# #setup# #request.foo# #isInitted#"; + } + + private function bar() { + return "whee"; + } + + function getThis() { + return this; + } + + function runThisFoo() { + return this.foo(); + } + + } \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/MyInterfaceBL.bx b/src/test/java/TestCases/asm/phase3/MyInterfaceBL.bx new file mode 100644 index 000000000..411c83543 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/MyInterfaceBL.bx @@ -0,0 +1,19 @@ +/** +* This is my interface description +* +* @brad wood +* @luis +*/ +interface singleton gavin="pickin" inject foo="bar" { + + function init(); + + function foo(); + + private function bar(); + + default function myDefaultMethod() { + return this.foo(); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/MyInterfaceCF.cfc b/src/test/java/TestCases/asm/phase3/MyInterfaceCF.cfc new file mode 100644 index 000000000..411c83543 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/MyInterfaceCF.cfc @@ -0,0 +1,19 @@ +/** +* This is my interface description +* +* @brad wood +* @luis +*/ +interface singleton gavin="pickin" inject foo="bar" { + + function init(); + + function foo(); + + private function bar(); + + default function myDefaultMethod() { + return this.foo(); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/MyInterfaceCFTag.cfc b/src/test/java/TestCases/asm/phase3/MyInterfaceCFTag.cfc new file mode 100644 index 000000000..255d4cda0 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/MyInterfaceCFTag.cfc @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/OnMissingMethod.cfc b/src/test/java/TestCases/asm/phase3/OnMissingMethod.cfc new file mode 100644 index 000000000..73e5af916 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/OnMissingMethod.cfc @@ -0,0 +1,5 @@ +component { + public any function onMissingMethod( string missingMethodName, any missingMethodArguments ){ + return missingMethodName & missingMethodArguments[2]; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Parent.cfc b/src/test/java/TestCases/asm/phase3/Parent.cfc new file mode 100644 index 000000000..0e435dab7 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Parent.cfc @@ -0,0 +1,12 @@ +component { + + function init() { + setupFrameworkDefaults(); + return this; + } + + private void function setupFrameworkDefaults() { + request.calls.append( "running parent setupFrameworkDefaults()" ); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/PropertyTest.bx b/src/test/java/TestCases/asm/phase3/PropertyTest.bx new file mode 100644 index 000000000..e27a9870e --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/PropertyTest.bx @@ -0,0 +1,25 @@ +class accessors=true { + @preAnno + property name="myProperty" default="myDefaultValue" type=string inject; + + /** + * This is my property + * @brad wood + * @luis + */ + @preanno "myValue" "anothervalue" + property string anotherprop; + + @ID + property theName; + + property string name; + + property shortcutWithDefault default="myDefaultValue"; + + property String typedShortcutWithDefault default="myDefaultValue2"; + + function init() { + getMyProperty(); + } +} diff --git a/src/test/java/TestCases/asm/phase3/PropertyTestCF.cfc b/src/test/java/TestCases/asm/phase3/PropertyTestCF.cfc new file mode 100644 index 000000000..420a38801 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/PropertyTestCF.cfc @@ -0,0 +1,18 @@ +component accessors=true { + property name="myProperty" default="myDefaultValue" type=string inject preAnno; + + /** + * This is my property + * @brad wood + * @luis + */ + property string anotherprop preanno=["myValue", "anothervalue"]; + + property shortcutWithDefault default="myDefaultValue"; + + property String typedShortcutWithDefault default="myDefaultValue2"; + + function init() { + getMyProperty(); + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/PseudoConstructorNoOutput.cfc b/src/test/java/TestCases/asm/phase3/PseudoConstructorNoOutput.cfc new file mode 100644 index 000000000..2c844bad6 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/PseudoConstructorNoOutput.cfc @@ -0,0 +1,3 @@ +component output=false { + echo( "PseudoConstructorOutput" ); +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/PseudoConstructorOutput.cfc b/src/test/java/TestCases/asm/phase3/PseudoConstructorOutput.cfc new file mode 100644 index 000000000..fe290b038 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/PseudoConstructorOutput.cfc @@ -0,0 +1,3 @@ +component output=true { + echo( "PseudoConstructorOutput" ); +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/RelativeInstantiation.bx b/src/test/java/TestCases/asm/phase3/RelativeInstantiation.bx new file mode 100644 index 000000000..4e7e792d5 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/RelativeInstantiation.bx @@ -0,0 +1,5 @@ +class { + function findSibling() { + return new FindMe().foo(); + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/SeniorVespa.bx b/src/test/java/TestCases/asm/phase3/SeniorVespa.bx new file mode 100644 index 000000000..1e1c8036b --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/SeniorVespa.bx @@ -0,0 +1,3 @@ +class extends="Moped" { + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/Stack.cfc b/src/test/java/TestCases/asm/phase3/Stack.cfc new file mode 100644 index 000000000..f3f5b33cd --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/Stack.cfc @@ -0,0 +1,10 @@ +component { + function init() { + stack = []; + return this; + } + + function empty() { + return stack.len() == 0; + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/StaticTest.bx b/src/test/java/TestCases/asm/phase3/StaticTest.bx new file mode 100644 index 000000000..51240db75 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/StaticTest.bx @@ -0,0 +1,33 @@ + class { + static { + static.scoped = "brad"; + unscoped = "wood" + static foo = 9000; + '123' = 456; + } + + static.foo = 42; + + static { + static.again = "luis" + } + + static function myStaticFunc() { + return "static" & static.foo; + } + + function myInstanceFunc() { + return "instance" & myStaticFunc(); + } + + array function myInstanceFunc2() { + return [static.scoped, + static.unscoped, + static.foo]; + } + + static function sayHello() { + return "Hello"; + } + + } \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/StaticTestCF.cfc b/src/test/java/TestCases/asm/phase3/StaticTestCF.cfc new file mode 100644 index 000000000..ebfad9dc3 --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/StaticTestCF.cfc @@ -0,0 +1,22 @@ +component { + static { + static.scoped = "brad"; + unscoped = "wood" + static foo = 9000; + } + + static.foo = 42; + + static { + static.again = "luis" + } + + static function myStaticFunc() { + return "static" & static.foo; + } + + function myInstanceFunc() { + return "instance" & myStaticFunc(); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/asm/phase3/WheeledThing.bx b/src/test/java/TestCases/asm/phase3/WheeledThing.bx new file mode 100644 index 000000000..bbbecf7cc --- /dev/null +++ b/src/test/java/TestCases/asm/phase3/WheeledThing.bx @@ -0,0 +1,13 @@ +class implements="IBicycle,IMotorcycle" { + + function onMissingMethod( name, args ) { + switch( arguments.name ) { + case "pedal": + return "Pedal speed #args[1]#" + break; + case "shift": + return "Shift to gear #args[1]#" + break; + } + } +} \ No newline at end of file diff --git a/src/test/java/TestCases/phase3/ApplicationTest.java b/src/test/java/TestCases/phase3/ApplicationTest.java index 6e1cd9abd..8aa47c085 100644 --- a/src/test/java/TestCases/phase3/ApplicationTest.java +++ b/src/test/java/TestCases/phase3/ApplicationTest.java @@ -20,8 +20,6 @@ import static com.google.common.truth.Truth.assertThat; import java.nio.file.Path; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -37,7 +35,6 @@ import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.RequestBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; -import ortus.boxlang.runtime.dynamic.casters.DateTimeCaster; import ortus.boxlang.runtime.scopes.ApplicationScope; import ortus.boxlang.runtime.scopes.IScope; import ortus.boxlang.runtime.scopes.Key; @@ -85,11 +82,8 @@ public void testBasicApplication() { assertThat( variables.get( result ) ).isInstanceOf( ApplicationScope.class ); assertThat( variables.get( Key.of( "result2" ) ) ).isInstanceOf( SessionScope.class ); - ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); - Application app = appContext.getApplication(); - Instant actual = DateTimeCaster.cast( variables.get( Key.of( "startTime" ) ) ).getWrapped().toInstant(); - Instant now = Instant.now(); - long differenceInSeconds = ChronoUnit.SECONDS.between( actual, now ); + ApplicationBoxContext appContext = context.getParentOfType( ApplicationBoxContext.class ); + Application app = appContext.getApplication(); assertThat( app.getName().getName() ).isEqualTo( "myAppsdfsdf" ); assertThat( app.getSessionsCache() ).isNotNull(); @@ -97,7 +91,6 @@ public void testBasicApplication() { assertThat( app.getApplicationScope().getName().getName() ).isEqualTo( "application" ); assertThat( app.getClassLoaders() ).isNotNull(); assertThat( app.hasStarted() ).isTrue(); - assertThat( differenceInSeconds ).isAtMost( 1L ); } @Test diff --git a/src/test/java/ortus/boxlang/compiler/LoadPrecompiledTemplateTest.java b/src/test/java/ortus/boxlang/compiler/LoadPrecompiledTemplateTest.java new file mode 100644 index 000000000..f1c41851a --- /dev/null +++ b/src/test/java/ortus/boxlang/compiler/LoadPrecompiledTemplateTest.java @@ -0,0 +1,121 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package ortus.boxlang.compiler; + +import static com.google.common.truth.Truth.assertThat; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.VariablesScope; + +public class LoadPrecompiledTemplateTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + } + + @Test + public void testPrecompiledTemplate() { + Path source = Path.of( "src/test/java/ortus/boxlang/compiler/Precompiled-source.bxs" ); + Path target = Path.of( "src/test/java/ortus/boxlang/compiler/Precompiled.bxs" ); + // delete target, if exists + if ( target.toFile().exists() ) { + target.toFile().delete(); + } + // copy source over target on disk + try { + Files.copy( source, target ); + } catch ( Exception e ) { + e.printStackTrace(); + } + + BXCompiler.compileFile( + target, + target, + true, + instance, + Path.of( "" ), + "/" + ); + instance.executeSource( + """ + include 'src/test/java/ortus/boxlang/compiler/Precompiled.bxs' + """, + context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( result ) ).isEqualTo( 4 ); + } + + @Test + public void testPrecompiledClass() { + Path source = Path.of( "src/test/java/ortus/boxlang/compiler/Precompiled-source.bx" ); + Path target = Path.of( "src/test/java/ortus/boxlang/compiler/Precompiled.bx" ); + // delete target, if exists + if ( target.toFile().exists() ) { + target.toFile().delete(); + } + // copy source over target on disk + try { + Files.copy( source, target ); + } catch ( Exception e ) { + e.printStackTrace(); + } + + BXCompiler.compileFile( + target, + target, + true, + instance, + Path.of( "" ), + "/" + ); + instance.executeSource( + """ + foo = new src.test.java.ortus.boxlang.compiler.Precompiled(); + result = foo.bar() + """, + context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( result ) ).isEqualTo( "brad" ); + + } +} diff --git a/src/test/java/ortus/boxlang/compiler/Precompiled-source.bx b/src/test/java/ortus/boxlang/compiler/Precompiled-source.bx new file mode 100644 index 000000000..7f9f4e1ad --- /dev/null +++ b/src/test/java/ortus/boxlang/compiler/Precompiled-source.bx @@ -0,0 +1,5 @@ +class { + function bar() { + return "brad"; + } +} \ No newline at end of file diff --git a/src/test/java/ortus/boxlang/compiler/Precompiled-source.bxs b/src/test/java/ortus/boxlang/compiler/Precompiled-source.bxs new file mode 100644 index 000000000..1b03bf6e6 --- /dev/null +++ b/src/test/java/ortus/boxlang/compiler/Precompiled-source.bxs @@ -0,0 +1 @@ +result = 2 + 2; \ No newline at end of file diff --git a/src/test/java/ortus/boxlang/compiler/Precompiled.bx b/src/test/java/ortus/boxlang/compiler/Precompiled.bx new file mode 100644 index 000000000..dab7fe845 Binary files /dev/null and b/src/test/java/ortus/boxlang/compiler/Precompiled.bx differ diff --git a/src/test/java/ortus/boxlang/compiler/Precompiled.bxs b/src/test/java/ortus/boxlang/compiler/Precompiled.bxs new file mode 100644 index 000000000..86cfa68aa Binary files /dev/null and b/src/test/java/ortus/boxlang/compiler/Precompiled.bxs differ diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/cache/CacheTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/cache/CacheTest.java index 04f73f219..5cb38a8c6 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/cache/CacheTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/cache/CacheTest.java @@ -46,13 +46,13 @@ public void canGetDefaultCache() { public void canGetNamedCache() { runtime.executeSource( """ - result = cache( "imports" ) + result = cache( "bxSessions" ) """, context ); ICacheProvider cache = ( ICacheProvider ) variables.get( result ); assertNotNull( cache ); - assertThat( cache.getName().getName() ).isEqualTo( "imports" ); + assertThat( cache.getName().getName() ).isEqualTo( "bxSessions" ); } } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/io/ExpandPathTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/io/ExpandPathTest.java index 428632b93..4a5108a53 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/io/ExpandPathTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/io/ExpandPathTest.java @@ -133,6 +133,25 @@ public void testMapping() { assertThat( variables.get( result ) ).isEqualTo( abs ); } + @Test + public void testFSCase() { + if ( FileSystemUtil.IS_WINDOWS ) { + // path relative to custom mapping + String abs; + try { + abs = Path.of( "src/test/java/ortus/boxlang/runtime/bifs/global/io/expandPathTest.txt" ).toAbsolutePath().toRealPath().toString(); + } catch ( IOException e ) { + throw new RuntimeException( e ); + } + instance.executeSource( + """ + result = ExpandPath( "/EXPAND/PATH/TEST/EXPANDPATHTEST.TXT" ); + """, + context ); + assertThat( variables.get( result ) ).isEqualTo( abs ); + } + } + @Test public void testRelative() { String abs = Path.of( "src/test/java/ortus/boxlang/runtime/bifs/global/io/expandPathTest.txt" ).toAbsolutePath().toString(); diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileCopyTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileCopyTest.java index 85c5f85c9..1ddf68623 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileCopyTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileCopyTest.java @@ -40,6 +40,7 @@ import ortus.boxlang.runtime.scopes.IScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.util.FileSystemUtil; public class FileCopyTest { @@ -139,4 +140,18 @@ public void testFileCopy() throws IOException { ); } + @DisplayName( "It tests the BIF FileCopy security" ) + @Test + public void testBifSecurity() { + variables.put( Key.of( "source" ), Path.of( sourceFile ).toAbsolutePath().toString() ); + assertThrows( + BoxRuntimeException.class, + () -> instance.executeSource( + """ + fileCopy( source, "blah.exe" ); + """, + context ) + ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileMoveTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileMoveTest.java index 59c1350af..bec9af550 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileMoveTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/io/FileMoveTest.java @@ -20,6 +20,7 @@ package ortus.boxlang.runtime.bifs.global.io; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -39,6 +40,7 @@ import ortus.boxlang.runtime.scopes.IScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.util.FileSystemUtil; public class FileMoveTest { @@ -54,6 +56,10 @@ public class FileMoveTest { @BeforeAll public static void setUp() { instance = BoxRuntime.getInstance( true ); + + // We are testing this here to ensure the default configuration is set up correctly + assertFalse( instance.getConfiguration().security.isExtensionAllowed( "exe" ) ); + } @AfterAll @@ -93,4 +99,18 @@ public void testBif() { assertTrue( FileSystemUtil.exists( destination ) ); } + @DisplayName( "It tests the BIF FileMove security" ) + @Test + public void testBifSecurity() { + variables.put( Key.of( "targetFile" ), Path.of( source ).toAbsolutePath().toString() ); + assertThrows( + BoxRuntimeException.class, + () -> instance.executeSource( + """ + fileMove( targetFile, "blah.exe" ); + """, + context ) + ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/list/ListFindTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/list/ListFindTest.java index 8db377b2a..daaa98043 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/list/ListFindTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/list/ListFindTest.java @@ -191,4 +191,17 @@ public void benchmark() { context ); } + @DisplayName( "It should not find partial" ) + @Test + public void testPartial() { + instance.executeSource( + """ + nums = "red,blue,orange"; + result = nums.listFindNoCase( "b" ); + """, + context ); + int found = ( int ) variables.get( result ); + assertThat( found ).isEqualTo( 0 ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/query/QueryRowDataTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/query/QueryRowDataTest.java index a60c8fa8f..615d19480 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/query/QueryRowDataTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/query/QueryRowDataTest.java @@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -48,11 +47,6 @@ public static void setUp() { instance = BoxRuntime.getInstance( true ); } - @AfterAll - public static void teardown() { - instance.shutdown(); - } - @BeforeEach public void setupEach() { context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncodeTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncodeTest.java index fcbebd4d6..5d9f035e3 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncodeTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/string/CharsetEncodeTest.java @@ -19,6 +19,7 @@ package ortus.boxlang.runtime.bifs.global.string; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.charset.Charset; @@ -80,4 +81,23 @@ public void testBif() { } + @Test + public void testByteArray() { + + instance.executeSource( + """ + result = charsetEncode( [0], "utf-8"); + """, + context ); + assertThat( variables.get( result ).equals( new byte[] { 0 } ) ); + + instance.executeSource( + """ + result = charsetEncode( javacast("byte[]", [0] ), "utf-8"); + """, + context ); + assertThat( variables.get( result ).equals( new byte[] { 0 } ) ); + + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/string/ReReplaceTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/string/ReReplaceTest.java index 6e05d97c4..5e428c001 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/string/ReReplaceTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/string/ReReplaceTest.java @@ -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.DisplayName; import org.junit.jupiter.api.Test; import ortus.boxlang.runtime.BoxRuntime; @@ -190,4 +191,15 @@ public void testPerlStyleCurlyLooseness() { // @formatter:on } + @DisplayName( "Doesn't throw exception if the string is null" ) + @Test + public void testNullString() { + instance.executeSource( + """ + result = reReplace( null, "[^a-z0-9]", '', "one" ); + """, + context ); + assertThat( variables.get( result ) ).isEqualTo( "" ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/JavaCastTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/system/JavaCastTest.java index 072ed0f15..40b8a9a40 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/JavaCastTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/JavaCastTest.java @@ -228,8 +228,8 @@ public void testItCastsToNativeArrayOfInt() { result = javaCast('int[]', [1,2,3]); """, context ); - assertThat( variables.get( result ) ).isInstanceOf( new Integer[] {}.getClass() ); - Integer[] castedArr = ( Integer[] ) variables.get( result ); + assertThat( variables.get( result ) ).isInstanceOf( new int[] {}.getClass() ); + int[] castedArr = ( int[] ) variables.get( result ); assertThat( castedArr[ 0 ] ).isEqualTo( 1 ); assertThat( castedArr[ 1 ] ).isEqualTo( 2 ); assertThat( castedArr[ 2 ] ).isEqualTo( 3 ); diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionInvalidateTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionInvalidateTest.java index 126dc0ff1..e1a1f27e7 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionInvalidateTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionInvalidateTest.java @@ -75,7 +75,7 @@ public void testBif() { context ); IStruct initialSession = variables.getAsStruct( Key.of( "initialSession" ) ); assertFalse( variables.getAsStruct( result ).containsKey( Key.of( "foo" ) ) ); - assertNotEquals( initialSession.getAsString( Key.of( "cfid" ) ), variables.getAsStruct( result ).getAsString( Key.of( "cfid" ) ) ); + assertNotEquals( initialSession.getAsString( Key.of( "jsessionID" ) ), variables.getAsStruct( result ).getAsString( Key.of( "jsessionID" ) ) ); assertNotEquals( initialSession.getAsDateTime( Key.of( "timeCreated" ) ), variables.getAsStruct( result ).getAsDateTime( Key.of( "timeCreated" ) ) ); assertNotEquals( initialSession.getAsString( Key.of( "sessionid" ) ), variables.getAsStruct( result ).getAsString( Key.of( "sessionid" ) ) ); } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionRotateTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionRotateTest.java index f053972c6..606077c41 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionRotateTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/SessionRotateTest.java @@ -80,7 +80,7 @@ public void testBif() { IStruct initialSession = variables.getAsStruct( Key.of( "initialSession" ) ); assertTrue( variables.getAsStruct( result ).containsKey( Key.of( "foo" ) ) ); - assertNotEquals( initialSession.getAsString( Key.of( "cfid" ) ), variables.getAsStruct( result ).getAsString( Key.of( "cfid" ) ) ); + assertNotEquals( initialSession.getAsString( Key.of( "jsessionID" ) ), variables.getAsStruct( result ).getAsString( Key.of( "jsessionID" ) ) ); assertNotEquals( initialSession.getAsDateTime( Key.of( "timeCreated" ) ), variables.getAsStruct( result ).getAsDateTime( Key.of( "timeCreated" ) ) ); assertNotEquals( initialSession.getAsString( Key.of( "sessionid" ) ), variables.getAsStruct( result ).getAsString( Key.of( "sessionid" ) ) ); } diff --git a/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java b/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java index b3756f950..210955900 100644 --- a/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java +++ b/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java @@ -78,8 +78,7 @@ void testItCanLoadTheCoreConfig() { assertThat( defaultCache.properties.get( "useLastAccessTimeouts" ) ).isEqualTo( true ); // Import Cache Checks - CacheConfig importCache = ( CacheConfig ) config.caches.get( "imports" ); - assertThat( importCache.name.getNameNoCase() ).isEqualTo( "IMPORTS" ); + CacheConfig importCache = ( CacheConfig ) config.caches.get( "bxImports" ); assertThat( importCache.provider.getNameNoCase() ).isEqualTo( "BOXCACHEPROVIDER" ); assertThat( importCache.properties ).isNotNull(); assertThat( importCache.properties.get( "maxObjects" ) ).isEqualTo( 200 ); @@ -190,8 +189,7 @@ private void assertConfigTest( Configuration config ) { assertThat( defaultCache.properties.get( "useLastAccessTimeouts" ) ).isEqualTo( true ); // Import Cache Checks - CacheConfig importCache = ( CacheConfig ) config.caches.get( "imports" ); - assertThat( importCache.name.getNameNoCase() ).isEqualTo( "IMPORTS" ); + CacheConfig importCache = ( CacheConfig ) config.caches.get( "bxImports" ); assertThat( importCache.provider.getNameNoCase() ).isEqualTo( "BOXCACHEPROVIDER" ); assertThat( importCache.properties ).isNotNull(); assertThat( importCache.properties.get( "maxObjects" ) ).isEqualTo( 200 ); diff --git a/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java b/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java index e5851fff9..eaf692096 100644 --- a/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java +++ b/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java @@ -17,6 +17,7 @@ */ package ortus.boxlang.runtime.config.segments; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -80,4 +81,12 @@ public void testIsImportDisallowed_DisallowedImport() { assertThrows( SecurityException.class, () -> securityConfig.isClassAllowed( "java.lang.String" ) ); } + @DisplayName( "Check if an exe extension is disallowed" ) + @Test + public void testIsExeDisalloweed() { + String fileName = "test.exe"; + securityConfig.disallowedFileOperationExtensions.add( "exe" ); + assertFalse( securityConfig.isFileOperationAllowed( fileName ) ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java b/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java index 6080ce7fe..f6cbe6813 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java @@ -28,7 +28,7 @@ public class ImportDefinitionTest { @DisplayName( "It can use default constructor" ) @Test public void testCanUseDefaultConstructor() { - ImportDefinition ImportDefinition = new ImportDefinition( "java.lang.String", "java", "jString" ); + ImportDefinition ImportDefinition = new ImportDefinition( "java.lang.String", "java", "jString", null ); assertThat( ImportDefinition.className() ).isEqualTo( "java.lang.String" ); assertThat( ImportDefinition.resolverPrefix() ).isEqualTo( "java" ); @@ -38,14 +38,12 @@ public void testCanUseDefaultConstructor() { @DisplayName( "It can use default constructor with nulls" ) @Test public void testCanUseDefaultConstructorWithNulls() { - ImportDefinition ImportDefinition = new ImportDefinition( "java.lang.String", null, null ); + ImportDefinition ImportDefinition = new ImportDefinition( "java.lang.String", null, null, null ); assertThat( ImportDefinition.className() ).isEqualTo( "java.lang.String" ); assertThat( ImportDefinition.resolverPrefix() ).isEqualTo( null ); assertThat( ImportDefinition.alias() ).isEqualTo( null ); - - assertThrows( Throwable.class, () -> new ImportDefinition( null, null, null ) ); - + assertThrows( Throwable.class, () -> new ImportDefinition( null, null, null, null ) ); } @DisplayName( "It can use static constructor parser" ) @@ -74,4 +72,34 @@ public void testCanUseStaticConstructorParser() { assertThat( importDef.getPackageName() ).isEqualTo( "java.net.http" ); } + @DisplayName( "It can parse a simple import from a module" ) + @Test + public void testCanParseSimpleImportFromModule() { + ImportDefinition importDef = ImportDefinition.parse( "java:java.lang.String@module" ); + assertThat( importDef.className() ).isEqualTo( "java.lang.String" ); + assertThat( importDef.resolverPrefix() ).isEqualTo( "java" ); + assertThat( importDef.alias() ).isEqualTo( "String" ); + assertThat( importDef.moduleName() ).isEqualTo( "module" ); + } + + @DisplayName( "It can parse a simple import from a module using an alias" ) + @Test + public void testCanParseSimpleImportFromModuleUsingAlias() { + ImportDefinition importDef = ImportDefinition.parse( "java:java.lang.String@module AS jString" ); + assertThat( importDef.className() ).isEqualTo( "java.lang.String" ); + assertThat( importDef.resolverPrefix() ).isEqualTo( "java" ); + assertThat( importDef.alias() ).isEqualTo( "jString" ); + assertThat( importDef.moduleName() ).isEqualTo( "module" ); + } + + @DisplayName( "It can parse a simple import from a module using wildcards" ) + @Test + public void testCanParseSimpleImportFromModuleUsingWildcards() { + ImportDefinition importDef = ImportDefinition.parse( "java:java.lang.*@module" ); + assertThat( importDef.className() ).isEqualTo( "java.lang.*" ); + assertThat( importDef.resolverPrefix() ).isEqualTo( "java" ); + assertThat( importDef.alias() ).isEqualTo( "*" ); + assertThat( importDef.moduleName() ).isEqualTo( "module" ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/AbstractResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/AbstractResolverTest.java new file mode 100644 index 000000000..53250ab0e --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/AbstractResolverTest.java @@ -0,0 +1,61 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.runtime.loader.resolvers; + +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.modules.ModuleRecord; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.services.ModuleService; + +public abstract class AbstractResolverTest { + + public static BoxRuntime runtime; + public static IBoxContext context; + + @BeforeAll + public static void beforeAll() { + runtime = BoxRuntime.getInstance( true ); + } + + @BeforeEach + public void beforeEach() { + context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); + } + + public void loadTestModule() { + Key moduleName = new Key( "test" ); + String physicalPath = Paths.get( "./modules/test" ).toAbsolutePath().toString(); + ModuleRecord moduleRecord = new ModuleRecord( physicalPath ); + ModuleService moduleService = runtime.getModuleService(); + + moduleRecord + .loadDescriptor( context ) + .register( context ) + .activate( context ); + + moduleService.getRegistry().put( moduleName, moduleRecord ); + } + +} diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java index 80f6bbe9f..f06736e38 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java @@ -20,93 +20,183 @@ import static com.google.common.truth.Truth.assertThat; -import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import ortus.boxlang.runtime.BoxRuntime; -import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.compiler.parser.BoxSourceType; import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.loader.ClassLocator.ClassLocation; +import ortus.boxlang.runtime.loader.ImportDefinition; +import ortus.boxlang.runtime.runnables.IBoxRunnable; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.DateTime; +import ortus.boxlang.runtime.util.ResolvedFilePath; -public class BoxResolverTest { +public class BoxResolverTest extends AbstractResolverTest { - static BoxRuntime runtime; - static IBoxContext context; - static BoxResolver boxResolver; + public static BoxResolver boxResolver; @BeforeAll public static void setUp() { - runtime = BoxRuntime.getInstance( true ); - context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); - boxResolver = runtime.getClassLocator().getBoxResolver(); - - // Create a mapping to the `resources` directory + boxResolver = runtime.getClassLocator().getBoxResolver(); + // Create a mapping to the `resources` directory as `/tests` Path resourcesDirectory = Paths.get( "src/test/resources" ).toAbsolutePath(); runtime.getConfiguration().registerMapping( "/tests", resourcesDirectory.toString() ); // System.out.println( "mappings: " + Arrays.toString( runtime.getConfiguration().getRegisteredMappings() ) ); } - @AfterAll - public static void teardown() { - } - - @DisplayName( "It can find be created" ) + @DisplayName( "It can be created correctly with the rigth prefix" ) @Test void testItCanBeCreated() { assertThat( boxResolver.getName() ).isEqualTo( "BoxResolver" ); assertThat( boxResolver.getPrefix() ).isEqualTo( "bx" ); } - @DisplayName( "It can find classes from modules" ) - @Test - @Disabled - void testFindFromModules() { - String className = "apppath.models.User"; // Example class name - assertThat( boxResolver.findFromModules( new ScriptingRequestBoxContext(), className, new ArrayList<>() ).isPresent() ).isFalse(); - } - @DisplayName( "It can find classes from local disk using a mapping" ) @Test - void testFindFromLocal() throws URISyntaxException { + void testFindFromLocal() { // You can find this in src/test/resources/tests/components/User.cfc String testComponent = "tests.components.User"; - - // System.out.println( "mappings: " + Arrays.toString( runtime.getConfiguration().getRegisteredMappings() ) ); - Optional classLocation = boxResolver.findFromLocal( context, testComponent, new ArrayList<>() ); assertThat( classLocation.isPresent() ).isTrue(); assertThat( classLocation.get().name() ).isEqualTo( "User" ); assertThat( classLocation.get().packageName() ).isEqualTo( "tests.components" ); - IClassRunnable cfc = ( IClassRunnable ) DynamicObject.of( classLocation.get().clazz() ) + IClassRunnable targetClass = ( IClassRunnable ) DynamicObject.of( classLocation.get().clazz() ) .invokeConstructor( context ) .getTargetInstance(); - assertThat( cfc ).isNotNull(); - assertThat( cfc.getThisScope().getAsString( Key.of( "name" ) ) ).isEqualTo( "test" ); - assertThat( cfc.getThisScope().get( Key.of( "created" ) ) ).isInstanceOf( DateTime.class ); + assertThat( targetClass ).isNotNull(); + assertThat( targetClass.getThisScope().getAsString( Key.of( "name" ) ) ).isEqualTo( "test" ); + assertThat( targetClass.getThisScope().get( Key.of( "created" ) ) ).isInstanceOf( DateTime.class ); } - @DisplayName( "It can resolve classes" ) + @DisplayName( "It can resolve classes using direct class names" ) @Test void testResolve() { - IBoxContext context = new ScriptingRequestBoxContext(); - String className = "apppath.models.User"; // Example class name - + // Invalid Class + String className = "apppath.models.User"; assertThat( boxResolver.resolve( context, className ).isPresent() ).isFalse(); + + // Now a class that exists + className = "src.test.bx.Person"; + assertThat( boxResolver.resolve( context, className ).isPresent() ).isTrue(); + assertThat( boxResolver.resolve( context, className ).get().path() ).contains( "Person" ); + } + + @DisplayName( "It can resolve classes using relative paths from a template executing it" ) + @Test + void testResolveRelativeFromTemplate() { + // Push a mock template + String mockPath = Path.of( "src/test/bx/index.bxm" ).toAbsolutePath().toString(); + context.pushTemplate( new IBoxRunnable() { + + @Override + public List getImports() { + return List.of(); + } + + @Override + public long getRunnableCompileVersion() { + return 0L; + } + + @Override + public LocalDateTime getRunnableCompiledOn() { + return null; + } + + @Override + public Object getRunnableAST() { + return null; + } + + @Override + public ResolvedFilePath getRunnablePath() { + return ResolvedFilePath.of( mockPath ); + } + + @Override + public BoxSourceType getSourceType() { + return BoxSourceType.BOXTEMPLATE; + } + + } ); + + try { + // Relative to the template above + String target = "Person"; + Optional classLocation = boxResolver.findFromLocal( context, target, new ArrayList<>() ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().name() ).isEqualTo( "Person" ); + + // Relative to the template above but embedded + target = "models.TestClass"; + classLocation = boxResolver.findFromLocal( context, target, new ArrayList<>() ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().name() ).isEqualTo( "TestClass" ); + } finally { + context.popTemplate(); + } + } + + @DisplayName( "It can resolve classes using direct imports" ) + @Test + void testResolveWithImports() { + String className = "TestClass"; + List imports = Arrays.asList( + ImportDefinition.parse( "src.test.bx.models.Validation" ), + ImportDefinition.parse( "src.test.bx.models.TestClass" ) + ); + Optional classLocation = boxResolver.resolve( context, className, imports ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().path() ).contains( "TestClass" ); + } + + @DisplayName( "It can resolve classes using wildcard imports" ) + @Test + void testResolveWithWildcardImports() { + String className = "TestClass"; + List imports = Arrays.asList( + ImportDefinition.parse( "src.test.bx.models.*" ) + ); + Optional classLocation = boxResolver.resolve( context, className, imports ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().path() ).contains( "TestClass" ); + } + + @DisplayName( "It can find classes from modules" ) + @Test + void testFindFromModules() { + loadTestModule(); + String className = "models.Hello@test"; + Optional classLocation = boxResolver.resolve( context, className, new ArrayList<>() ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().name() ).isEqualTo( "Hello" ); + } + + @DisplayName( "It can resolve classes using imports for modules" ) + @Test + void testResolveWithImportsForModules() { + loadTestModule(); + String className = "Hello"; + List imports = Arrays.asList( + ImportDefinition.parse( "models.Hello@test" ), + ImportDefinition.parse( "src.test.bx.models.TestClass" ) + ); + Optional classLocation = boxResolver.resolve( context, className, imports ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().path().replace( "\\", "/" ) ).contains( "test/models/Hello" ); } } diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java index ea1edf84a..5c6e05b48 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java @@ -24,19 +24,18 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.ConsoleHandler; import org.apache.commons.lang3.ClassUtils; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; import ortus.boxlang.runtime.loader.ClassLocator; @@ -45,26 +44,20 @@ import ortus.boxlang.runtime.loader.ImportDefinition; import ortus.boxlang.runtime.types.IStruct; -public class JavaResolverTest { +public class JavaResolverTest extends AbstractResolverTest { - static BoxRuntime runtime; - static JavaResolver javaResolver; - static IBoxContext context; + public static JavaResolver javaResolver; @BeforeAll - public static void setUp() { - runtime = BoxRuntime.getInstance( true ); - javaResolver = runtime.getClassLocator().getJavaResolver(); + public static void beforeAll() { + javaResolver = runtime.getClassLocator().getJavaResolver(); } @BeforeEach - public void setup() { - context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); - } - - @AfterAll - public static void teardown() { - + @Override + public void beforeEach() { + super.beforeEach(); + javaResolver.clearJdkImportCache(); } @DisplayName( "It can find be created" ) @@ -132,39 +125,113 @@ public void testFindFromDependentLibraries() { @DisplayName( "It can resolve classes" ) @Test - public void testResolve() { - String className = "org.apache.commons.lang3.ClassUtils"; + public void testResolveClasses() { + String className = "java.util.HashSet"; Optional classLocation = javaResolver.findFromSystem( className, new ArrayList<>(), context ); assertThat( classLocation.isPresent() ).isTrue(); - assertThat( classLocation.get().clazz() ).isEqualTo( ClassUtils.class ); - assertThat( classLocation.get().name() ).isEqualTo( "ClassUtils" ); - assertThat( classLocation.get().packageName() ).isEqualTo( "org.apache.commons.lang3" ); + assertThat( classLocation.get().clazz() ).isEqualTo( HashSet.class ); + assertThat( classLocation.get().name() ).isEqualTo( "HashSet" ); + assertThat( classLocation.get().packageName() ).isEqualTo( "java.util" ); assertThat( classLocation.get().type() ).isEqualTo( ClassLocator.TYPE_JAVA ); assertThat( classLocation.get().module() ).isNull(); } + @DisplayName( "It can resolve classes using aliases" ) + @Test + public void testResolveWithAliases() { + String className = "MySet"; + List imports = Arrays.asList( + ImportDefinition.parse( "java:java.util.HashSet as MySet" ) + ); + Optional classLocation = javaResolver.resolve( context, className, imports ); + + System.out.println( classLocation ); + + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().clazz() ).isEqualTo( HashSet.class ); + assertThat( classLocation.get().name() ).isEqualTo( "HashSet" ); + assertThat( classLocation.get().packageName() ).isEqualTo( "java.util" ); + assertThat( classLocation.get().type() ).isEqualTo( ClassLocator.TYPE_JAVA ); + } + + @DisplayName( "It can resolve a module class by resolution and not explicitly" ) + @Test + public void testResolveModuleClass() { + loadTestModule(); + String className = "com.ortussolutions.bifs.Hola"; + List imports = Arrays.asList( + ImportDefinition.parse( "java:java.util.HashSet as MySet" ) + ); + Optional classLocation = javaResolver.resolve( context, className, imports ); + + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().name() ).isEqualTo( "Hola" ); + assertThat( classLocation.get().packageName() ).isEqualTo( "com.ortussolutions.bifs" ); + assertThat( classLocation.get().type() ).isEqualTo( ClassLocator.TYPE_JAVA ); + assertThat( classLocation.get().module() ).isEqualTo( "test" ); + } + + @DisplayName( "It can resolve a module class explicitly via an import" ) + @Test + public void testResolveModuleClassExplicitly() { + loadTestModule(); + String className = "Hola"; + List imports = Arrays.asList( + ImportDefinition.parse( "java:com.ortussolutions.bifs.Hola@test" ) + ); + Optional classLocation = javaResolver.resolve( context, className, imports ); + + // System.out.println( classLocation ); + assertThat( classLocation.isPresent() ).isTrue(); + assertThat( classLocation.get().name() ).isEqualTo( "Hola" ); + assertThat( classLocation.get().packageName() ).isEqualTo( "com.ortussolutions.bifs" ); + assertThat( classLocation.get().type() ).isEqualTo( ClassLocator.TYPE_JAVA ); + assertThat( classLocation.get().module() ).isEqualTo( "test" ); + } + @DisplayName( "It can resolve wildcard imports from the JDK itself" ) @Test - void testItCanResolveWildcardImports() throws Exception { + void testItCanResolveWildcardImports() { List imports = Arrays.asList( ImportDefinition.parse( "java:java.lang.*" ), ImportDefinition.parse( "java:java.util.*" ) ); - javaResolver.clearJdkImportCache(); assertThat( javaResolver.getJdkImportCacheSize() ).isEqualTo( 0 ); - String fqn = javaResolver.expandFromImport( new ScriptingRequestBoxContext(), "String", imports ); + String fqn = javaResolver.expandFromImport( context, "String", imports ); assertThat( fqn ).isEqualTo( "java.lang.String" ); - fqn = javaResolver.expandFromImport( new ScriptingRequestBoxContext(), "Integer", imports ); + fqn = javaResolver.expandFromImport( context, "Integer", imports ); assertThat( fqn ).isEqualTo( "java.lang.Integer" ); - fqn = javaResolver.expandFromImport( new ScriptingRequestBoxContext(), "List", imports ); + fqn = javaResolver.expandFromImport( context, "List", imports ); assertThat( fqn ).isEqualTo( "java.util.List" ); } + @DisplayName( "It can resolve wildcard imports from NON JDK classes on disk path" ) + @Test + void testItCanResolveWildcardImportsFromNonJDK() { + List imports = Arrays.asList( + ImportDefinition.parse( "java:ortus.boxlang.runtime.util.*" ) + ); + + String fqn = javaResolver.expandFromImport( context, "DumpUtil", imports ); + assertThat( fqn ).isEqualTo( "ortus.boxlang.runtime.util.DumpUtil" ); + } + + @DisplayName( "It can resolve wildcard imports from NON JDK classes in a JAR" ) + @Test + void testItCanResolveWildcardImportsFromNonJDKInAJar() { + List imports = Arrays.asList( + ImportDefinition.parse( "java:com.zaxxer.hikari.util.*" ) + ); + + String fqn = javaResolver.expandFromImport( context, "ConcurrentBag", imports ); + assertThat( fqn ).isEqualTo( "com.zaxxer.hikari.util.ConcurrentBag" ); + } + @DisplayName( "It can load libs from the 'home/libs' convention" ) @Test void testItCanLoadLibsFromHomeLibs() throws IOException { diff --git a/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java b/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java index 255d3d0ce..08d195910 100644 --- a/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java +++ b/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import ortus.boxlang.compiler.parser.BoxSourceType; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.config.segments.ModuleConfig; import ortus.boxlang.runtime.context.IBoxContext; @@ -250,5 +251,17 @@ void testItCanActivateAModule() throws ClassNotFoundException { "Hello World, my name is boxlang and I am 0 years old" ); // assertThat( variables.getAsString( Key.of( "result4" ) ) ).isEqualTo( "Hola Mundo!" ); + + // Test Module Class Locators + // @formatter:off + runtime.executeSource(""" + helloClass = createObject( "models.Hello@test" ) + result = helloClass.sayHello(); + """, + context, + BoxSourceType.BOXSCRIPT + ); + // @formatter:on + assertThat( variables.getAsString( Key.result ) ).contains( "ModuleLand" ); } } diff --git a/src/test/java/ortus/boxlang/runtime/operators/CastAsTest.java b/src/test/java/ortus/boxlang/runtime/operators/CastAsTest.java index 918f180d7..82413965b 100644 --- a/src/test/java/ortus/boxlang/runtime/operators/CastAsTest.java +++ b/src/test/java/ortus/boxlang/runtime/operators/CastAsTest.java @@ -31,6 +31,7 @@ import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.runnables.RunnableLoader; +import ortus.boxlang.runtime.types.Array; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.types.util.MathUtil; @@ -234,6 +235,58 @@ void testItCanCastArrayToArray() { assertThat( arrResult[ 1 ] ).isEqualTo( "Wood" ); } + @DisplayName( "It can cast primitive Array to boxed array" ) + @Test + void testItCanCastPrimitiveArrayToBoxedArray() { + Object result = CastAs.invoke( context, new int[] { 1, 2, 3 }, "Integer[]" ); + assertThat( result.getClass().isArray() ).isTrue(); + assertThat( result ).isInstanceOf( Integer[].class ); + Integer[] arrResult = ( ( Integer[] ) result ); + assertThat( arrResult.length ).isEqualTo( 3 ); + assertThat( arrResult[ 0 ] ).isEqualTo( 1 ); + assertThat( arrResult[ 1 ] ).isEqualTo( 2 ); + assertThat( arrResult[ 2 ] ).isEqualTo( 3 ); + } + + @DisplayName( "It can cast primitive Array to primitive array" ) + @Test + void testItCanCastPrimitiveArrayToPrimitiveArray() { + Object result = CastAs.invoke( context, new int[] { 1, 2, 3 }, "int[]" ); + assertThat( result.getClass().isArray() ).isTrue(); + assertThat( result ).isInstanceOf( int[].class ); + int[] arrResult = ( ( int[] ) result ); + assertThat( arrResult.length ).isEqualTo( 3 ); + assertThat( arrResult[ 0 ] ).isEqualTo( 1 ); + assertThat( arrResult[ 1 ] ).isEqualTo( 2 ); + assertThat( arrResult[ 2 ] ).isEqualTo( 3 ); + } + + @DisplayName( "It can cast boxed Array to boxed array" ) + @Test + void testItCanCastBoxedArrayToBoxedArray() { + Object result = CastAs.invoke( context, new Integer[] { 1, 2, 3 }, "Integer[]" ); + assertThat( result.getClass().isArray() ).isTrue(); + assertThat( result ).isInstanceOf( Integer[].class ); + Integer[] arrResult = ( ( Integer[] ) result ); + assertThat( arrResult.length ).isEqualTo( 3 ); + assertThat( arrResult[ 0 ] ).isEqualTo( 1 ); + assertThat( arrResult[ 1 ] ).isEqualTo( 2 ); + assertThat( arrResult[ 2 ] ).isEqualTo( 3 ); + } + + @DisplayName( "It can cast boxed Array to primitive array" ) + @Test + void testItCanCastBoxedArrayToPrimitiveArray() { + Object result = CastAs.invoke( context, new Integer[] { 1, 2, 3 }, "int[]" ); + assertThat( result.getClass().isArray() ).isTrue(); + assertThat( result ).isInstanceOf( int[].class ); + int[] arrResult = ( ( int[] ) result ); + assertThat( arrResult.length ).isEqualTo( 3 ); + assertThat( arrResult[ 0 ] ).isEqualTo( 1 ); + assertThat( arrResult[ 1 ] ).isEqualTo( 2 ); + assertThat( arrResult[ 2 ] ).isEqualTo( 3 ); + } + @DisplayName( "It can cast List to array" ) @Test void testItCanCastListToArray() { @@ -247,6 +300,19 @@ void testItCanCastListToArray() { assertThat( arrResult[ 1 ] ).isEqualTo( "Wood" ); } + @DisplayName( "It can cast array to array of primitives" ) + @Test + void testItCanCastArrayToArrayOfPrimitives() { + Object result = CastAs.invoke( context, Array.of( 0, 1, 2 ), "byte[]" ); + assertThat( result.getClass().isArray() ).isTrue(); + assertThat( result ).isInstanceOf( byte[].class ); + byte[] arrResult = ( ( byte[] ) result ); + assertThat( arrResult.length ).isEqualTo( 3 ); + assertThat( arrResult[ 0 ] ).isEqualTo( 0 ); + assertThat( arrResult[ 1 ] ).isEqualTo( 1 ); + assertThat( arrResult[ 2 ] ).isEqualTo( 2 ); + } + @DisplayName( "It can cast class to class" ) @Test void testItCanCastClassToClass() { diff --git a/src/test/resources/test-boxlang.json b/src/test/resources/test-boxlang.json index ac394ffcb..ab24b65a9 100644 --- a/src/test/resources/test-boxlang.json +++ b/src/test/resources/test-boxlang.json @@ -22,7 +22,8 @@ "useLastAccessTimeouts": true }, "caches": { - "imports": { + "default": {}, + "bxImports": { "provider": "BoxCacheProvider", "properties": { "evictCount": 1,