diff --git a/.github/workflows/cn1playground-language.yml b/.github/workflows/cn1playground-language.yml new file mode 100644 index 0000000000..de946be799 --- /dev/null +++ b/.github/workflows/cn1playground-language.yml @@ -0,0 +1,47 @@ +name: CN1 Playground Language Tests + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'scripts/cn1playground/**' + - '.github/workflows/cn1playground-language.yml' + push: + branches: [main, master] + paths: + - 'scripts/cn1playground/**' + - '.github/workflows/cn1playground-language.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + language-smoke: + name: Playground language smoke + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + + - name: Ensure Xvfb is available + run: | + set -euo pipefail + if ! command -v xvfb-run >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y xvfb + fi + + - name: Run playground language smoke tests + run: | + set -euo pipefail + cd scripts/cn1playground + xvfb-run -a bash tools/run-playground-smoke-tests.sh diff --git a/scripts/cn1playground/README.md b/scripts/cn1playground/README.md index 46f1d0e35c..414c493a5e 100644 --- a/scripts/cn1playground/README.md +++ b/scripts/cn1playground/README.md @@ -347,9 +347,37 @@ mvn clean install ```bash cd scripts/cn1playground -./scripts/run-tests.sh +bash tools/run-playground-smoke-tests.sh ``` +This smoke command currently runs: + +1. CN1 access registry generation (`tools/generate-cn1-access-registry.sh`). +2. Registry sanity checks (expected/forbidden class entries). +3. `PlaygroundSmokeHarness` end-to-end behavior checks. +4. `PlaygroundSyntaxMatrixHarness` syntax regression checks. + +## Language Feature Rollout Process + +Use this process when adding or fixing Java syntax support in Playground: + +1. **Add/adjust matrix coverage first** + Update `common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java` with a focused snippet for the target syntax. + - For currently unsupported syntax, add as `ExpectedOutcome.FAILURE`. + - When support lands, flip that case to `ExpectedOutcome.SUCCESS`. + +2. **Implement parser/runtime change in small steps** + Prefer one syntax feature per PR (e.g. method references only) to keep regressions easy to isolate. + +3. **Run smoke + syntax matrix locally** + Run `bash tools/run-playground-smoke-tests.sh` from `scripts/cn1playground`. + +4. **Require CI green before merge** + The `CN1 Playground Language Tests` workflow runs the same smoke command under CI (`xvfb-run`) and should pass before merging syntax updates. + +5. **Document behavior changes** + Update this README's known issues/limitations when syntax support changes so users know what is now supported. + ## Known Issues 1. **Parse errors with complex expressions**: BeanShell's parser may fail on some Java syntax. Simplify complex expressions or break them into multiple statements. @@ -360,4 +388,4 @@ cd scripts/cn1playground ## Contributing -See the main Codename One repository for contribution guidelines. \ No newline at end of file +See the main Codename One repository for contribution guidelines. diff --git a/scripts/cn1playground/common/src/main/java/bsh/BSHEnhancedForStatement.java b/scripts/cn1playground/common/src/main/java/bsh/BSHEnhancedForStatement.java index 02788ca21b..b194b0cc32 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/BSHEnhancedForStatement.java +++ b/scripts/cn1playground/common/src/main/java/bsh/BSHEnhancedForStatement.java @@ -67,6 +67,9 @@ public Object eval(CallStack callstack, Interpreter interpreter) throws EvalErro statement = nodeCount > 1 ? jjtGetChild(1) : null; } final Object iteratee = expression.eval(callstack, interpreter); + if (iteratee == null || iteratee == Primitive.NULL) { + throw new EvalException("Cannot iterate over null value", this, callstack); + } final CollectionManager cm = CollectionManager.getCollectionManager(); final Iterator iterator = cm.getBshIterator(iteratee); try { diff --git a/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java b/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java index d1f4a1488f..17fe6c3f87 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java +++ b/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java @@ -15,54 +15,285 @@ * KIND, either express or implied. See the License for the * * specific language governing permissions and limitations * * under the License. * + * * + * * + * This file is part of the BeanShell Java Scripting distribution. * + * Documentation and updates may be found at http://www.beanshell.org/ * + * Patrick Niemeyer (pat@pat.net) * + * Author of Learning Java, O'Reilly & Associates * + * * *****************************************************************************/ - package bsh; -/** - * Scripted class generation is not supported in the CN1 playground runtime. - * This stub preserves parser/runtime references while failing explicitly if a - * script attempts to declare or synthesize classes. - */ +import static bsh.This.Keys.BSHSUPER; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + public final class ClassGenerator { - public enum Type { - CLASS, INTERFACE, ENUM - } - public static final class ClassNodeFilter implements BSHBlock.NodeFilter { - public static final ClassNodeFilter CLASSSTATICFIELDS = new ClassNodeFilter(); - public static final ClassNodeFilter CLASSSTATICMETHODS = new ClassNodeFilter(); - public static final ClassNodeFilter CLASSINSTANCEFIELDS = new ClassNodeFilter(); - public static final ClassNodeFilter CLASSINSTANCEMETHODS = new ClassNodeFilter(); - public static final ClassNodeFilter CLASSCLASSES = new ClassNodeFilter(); + enum Type { CLASS, INTERFACE, ENUM } - private ClassNodeFilter() { + private static ClassGenerator cg; + + public static ClassGenerator getClassGenerator() { + if (cg == null) { + cg = new ClassGenerator(); } - public boolean isVisible(Node node) { - return false; + return cg; + } + + /** + * Parse the BSHBlock for the class definition and generate the class. + */ + public Class generateClass(String name, Modifiers modifiers, Class[] interfaces, Class superClass, BSHBlock block, Type type, CallStack callstack, Interpreter interpreter) throws EvalError { + // Delegate to the static method + return generateClassImpl(name, modifiers, interfaces, superClass, block, type, callstack, interpreter); + } + + /** + * Invoke a super.method() style superclass method on an object instance. + * This is not a normal function of the Java reflection API and is + * provided by generated class accessor methods. + */ + public Object invokeSuperclassMethod(BshClassManager bcm, Object instance, Class classStatic, String methodName, Object[] args) throws UtilEvalError, ReflectError, InvocationTargetException { + // Delegate to the static method + return invokeSuperclassMethodImpl(bcm, instance, classStatic, methodName, args); + } + + /** + * Parse the BSHBlock for for the class definition and generate the class + * using ClassGenerator. + */ + public static Class generateClassImpl(String name, Modifiers modifiers, Class[] interfaces, Class superClass, BSHBlock block, Type type, CallStack callstack, Interpreter interpreter) throws EvalError { + NameSpace enclosingNameSpace = callstack.top(); + String packageName = enclosingNameSpace.getPackage(); + String className = enclosingNameSpace.isClass ? (enclosingNameSpace.getName() + "$" + name) : name; + String fqClassName = packageName == null ? className : packageName + "." + className; + BshClassManager bcm = interpreter.getClassManager(); + + // Create the class static namespace + NameSpace classStaticNameSpace = new NameSpace(enclosingNameSpace, className); + classStaticNameSpace.isClass = true; + + callstack.push(classStaticNameSpace); + + // Evaluate any inner class class definitions in the block + // effectively recursively call this method for contained classes first + block.evalBlock(callstack, interpreter, true/*override*/, ClassNodeFilter.CLASSCLASSES); + + // Generate the type for our class + Variable[] variables = getDeclaredVariables(block, callstack, interpreter, packageName); + DelayedEvalBshMethod[] methods = getDeclaredMethods(block, callstack, interpreter, packageName, superClass); + + callstack.pop(); + + // initialize static this singleton in namespace + classStaticNameSpace.getThis(interpreter); + + // Create the class generator, which encapsulates all knowledge of the + // structure of the class + ClassGeneratorUtil classGenerator = new ClassGeneratorUtil(modifiers, className, packageName, superClass, interfaces, variables, methods, classStaticNameSpace, type); + + // Let the class generator install hooks relating to the structure of + // the class into the class static namespace. e.g. the constructor + // array. This is necessary whether we are generating code or just + // reinitializing a previously generated class. + classGenerator.initStaticNameSpace(classStaticNameSpace, block/*instance initializer*/); + + // Check for existing class (saved class file) + Class genClass = bcm.getAssociatedClass(fqClassName); + + // If the class isn't there then generate it. + // Else just let it be initialized below. + if (genClass == null) { + // generate bytecode, optionally with static init hooks to + // bootstrap the interpreter + byte[] code = classGenerator.generateClass(); + + if (Interpreter.getSaveClasses()) { + Interpreter.debug("Class file persistence is not available in the Codename One playground runtime."); + } + + // Define the new class in the classloader + genClass = bcm.defineClass(fqClassName, code); + Interpreter.debug("Define ", fqClassName, " as ", genClass); } + // import the unqualified class name into parent namespace + enclosingNameSpace.importClass(fqClassName.replace('$', '.')); + + // Give the static space its class static import + // important to do this after all classes are defined + classStaticNameSpace.setClassStatic(genClass); + + Interpreter.debug(classStaticNameSpace); + + if (interpreter.getStrictJava()) + ClassGeneratorUtil.checkAbstractMethodImplementation(genClass); + + return genClass; } - private static final ClassGenerator INSTANCE = new ClassGenerator(); + static Variable[] getDeclaredVariables(BSHBlock body, CallStack callstack, Interpreter interpreter, String defaultPackage) { + List vars = new ArrayList(); + for (int child = 0; child < body.jjtGetNumChildren(); child++) { + Node node = body.jjtGetChild(child); + if (node instanceof BSHEnumConstant) { + BSHEnumConstant enm = (BSHEnumConstant) node; + try { + Variable var = new Variable(enm.getName(), + enm.getType(), null/*value*/, enm.mods); + vars.add(var); + } catch (UtilEvalError e) { + // value error shouldn't happen + } + } else if (node instanceof BSHTypedVariableDeclaration) { + BSHTypedVariableDeclaration tvd = (BSHTypedVariableDeclaration) node; + Modifiers modifiers = tvd.modifiers; + BSHVariableDeclarator[] vardec = tvd.getDeclarators(); + for (BSHVariableDeclarator aVardec : vardec) { + String name = aVardec.name; + try { + Class type = tvd.evalType(callstack, interpreter); + Variable var = new Variable(name, type, null/*value*/, modifiers); + vars.add(var); + } catch (UtilEvalError | EvalError e) { + // value error shouldn't happen + } + } + } + } - private ClassGenerator() { + return vars.toArray(new Variable[vars.size()]); } - public static ClassGenerator getClassGenerator() { - return INSTANCE; + static DelayedEvalBshMethod[] getDeclaredMethods(BSHBlock body, + CallStack callstack, Interpreter interpreter, String defaultPackage, + Class superClass) throws EvalError { + List methods = new ArrayList<>(); + if ( callstack.top().getName().indexOf("$anon") > -1 ) { + // anonymous classes need super constructor + String classBaseName = Types.getBaseName(callstack.top().getName()); + Invocable con = BshClassManager.memberCache.get(superClass) + .findMethod(superClass.getName(), + This.CONTEXT_ARGS.get().get(classBaseName)); + DelayedEvalBshMethod bm = new DelayedEvalBshMethod(classBaseName, con, callstack.top()); + methods.add(bm); + } + for (int child = 0; child < body.jjtGetNumChildren(); child++) { + Node node = body.jjtGetChild(child); + if (node instanceof BSHMethodDeclaration) { + BSHMethodDeclaration md = (BSHMethodDeclaration) node; + md.insureNodesParsed(); + Modifiers modifiers = md.modifiers; + String name = md.name; + String returnType = md.getReturnTypeDescriptor(callstack, interpreter, defaultPackage); + BSHReturnType returnTypeNode = md.getReturnTypeNode(); + BSHFormalParameters paramTypesNode = md.paramsNode; + String[] paramTypes = paramTypesNode.getTypeDescriptors(callstack, interpreter, defaultPackage); + + DelayedEvalBshMethod bm = new DelayedEvalBshMethod(name, returnType, returnTypeNode, md.paramsNode.getParamNames(), paramTypes, paramTypesNode, md.blockNode, null/*declaringNameSpace*/, modifiers, md.isVarArgs, callstack, interpreter); + + methods.add(bm); + } + } + return methods.toArray(new DelayedEvalBshMethod[methods.size()]); } - public Class generateClass(String name, Modifiers modifiers, Class[] interfaces, - Class superClass, BSHBlock block, Type type, CallStack callstack, - Interpreter interpreter) throws EvalError { - throw new EvalError("Scripted class generation is not supported in the Codename One BeanShell runtime.", - null, callstack); + + /** + * A node filter that filters nodes for either a class body static + * initializer or instance initializer. In the static case only static + * members are passed, etc. + */ + static class ClassNodeFilter implements BSHBlock.NodeFilter { + private enum Context { STATIC, INSTANCE, CLASSES } + private enum Types { ALL, METHODS, FIELDS } + public static ClassNodeFilter CLASSSTATICFIELDS = new ClassNodeFilter(Context.STATIC, Types.FIELDS); + public static ClassNodeFilter CLASSSTATICMETHODS = new ClassNodeFilter(Context.STATIC, Types.METHODS); + public static ClassNodeFilter CLASSINSTANCEFIELDS = new ClassNodeFilter(Context.INSTANCE, Types.FIELDS); + public static ClassNodeFilter CLASSINSTANCEMETHODS = new ClassNodeFilter(Context.INSTANCE, Types.METHODS); + public static ClassNodeFilter CLASSCLASSES = new ClassNodeFilter(Context.CLASSES); + + Context context; + Types types = Types.ALL; + + private ClassNodeFilter(Context context) { + this.context = context; + } + + private ClassNodeFilter(Context context, Types types) { + this.context = context; + this.types = types; + } + + @Override + public boolean isVisible(Node node) { + if (context == Context.CLASSES) return node instanceof BSHClassDeclaration; + + // Only show class decs in CLASSES + if (node instanceof BSHClassDeclaration) return false; + + if (context == Context.STATIC) + return types == Types.METHODS ? isStaticMethod(node) : isStatic(node); + + // context == Context.INSTANCE cannot be anything else + return types == Types.METHODS ? isInstanceMethod(node) : isNonStatic(node); + } + + private boolean isStatic(Node node) { + if ( node.jjtGetParent().jjtGetParent() instanceof BSHClassDeclaration + && ((BSHClassDeclaration) node.jjtGetParent().jjtGetParent()).type == Type.INTERFACE ) + return true; + + if (node instanceof BSHTypedVariableDeclaration) + return ((BSHTypedVariableDeclaration) node).modifiers.hasModifier("static"); + + if (node instanceof BSHBlock) + return ((BSHBlock) node).isStatic; + + return false; + } + + private boolean isNonStatic(Node node) { + if (node instanceof BSHMethodDeclaration) + return false; + return !isStatic(node); + } + + private boolean isStaticMethod(Node node) { + if (node instanceof BSHMethodDeclaration) + return ((BSHMethodDeclaration) node).modifiers.hasModifier("static"); + return false; + } + + private boolean isInstanceMethod(Node node) { + if (node instanceof BSHMethodDeclaration) + return !((BSHMethodDeclaration) node).modifiers.hasModifier("static"); + return false; + } } - public Object invokeSuperclassMethod(BshClassManager bcm, Object instance, - Class superClass, String methodName, Object[] args) throws EvalError { - throw new EvalError("Superclass dispatch for generated classes is not supported in the Codename One BeanShell runtime.", - null, null); + /** Find and invoke the super class delegate method. */ + public static Object invokeSuperclassMethodImpl(BshClassManager bcm, + Object instance, Class classStatic, String methodName, Object[] args) + throws UtilEvalError, ReflectError, InvocationTargetException { + Class superClass = classStatic.getSuperclass(); + Class clas = instance.getClass(); + String superName = BSHSUPER + superClass.getSimpleName() + methodName; + + // look for the specially named super delegate method + Invocable superMethod = Reflect.resolveJavaMethod(clas, superName, + Types.getTypes(args), false/*onlyStatic*/); + if (superMethod != null) return superMethod.invoke(instance, args); + + // No super method, try to invoke regular method + // could be a superfluous "super." which is legal. + superMethod = Reflect.resolveExpectedJavaMethod(bcm, superClass, instance, + methodName, args, false/*onlyStatic*/); + return superMethod.invoke(instance, args); } + } diff --git a/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java b/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java index 8483355285..ca4d8871b0 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java +++ b/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java @@ -1,11 +1,1003 @@ +/***************************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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. * + * * + * * + * This file is part of the BeanShell Java Scripting distribution. * + * Documentation and updates may be found at http://www.beanshell.org/ * + * Patrick Niemeyer (pat@pat.net) * + * Author of Learning Java, O'Reilly & Associates * + * * + *****************************************************************************/ + package bsh; +import static bsh.ClassGenerator.Type.CLASS; +import static bsh.ClassGenerator.Type.ENUM; +import static bsh.ClassGenerator.Type.INTERFACE; +import static bsh.This.Keys.BSHCLASSMODIFIERS; +import static bsh.This.Keys.BSHCONSTRUCTORS; +import static bsh.This.Keys.BSHINIT; +import static bsh.This.Keys.BSHSTATIC; +import static bsh.This.Keys.BSHTHIS; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import bsh.org.objectweb.asm.ClassWriter; +import bsh.org.objectweb.asm.Label; +import bsh.org.objectweb.asm.MethodVisitor; +import bsh.org.objectweb.asm.Opcodes; +import bsh.org.objectweb.asm.Type; + /** - * Placeholder for the disabled class-generation pipeline. + * ClassGeneratorUtil utilizes the ASM (www.objectweb.org) bytecode generator + * by Eric Bruneton in order to generate class "stubs" for BeanShell at + * runtime. + *

+ *

+ * Stub classes contain all of the fields of a BeanShell scripted class + * as well as two "callback" references to BeanShell namespaces: one for + * static methods and one for instance methods. Methods of the class are + * delegators which invoke corresponding methods on either the static or + * instance bsh object and then unpack and return the results. The static + * namespace utilizes a static import to delegate variable access to the + * class' static fields. The instance namespace utilizes a dynamic import + * (i.e. mixin) to delegate variable access to the class' instance variables. + *

+ *

+ * Constructors for the class delegate to the static initInstance() method of + * ClassGeneratorUtil to initialize new instances of the object. initInstance() + * invokes the instance intializer code (init vars and instance blocks) and + * then delegates to the corresponding scripted constructor method in the + * instance namespace. Constructors contain special switch logic which allows + * the BeanShell to control the calling of alternate constructors (this() or + * super() references) at runtime. + *

+ *

+ * Specially named superclass delegator methods are also generated in order to + * allow BeanShell to access overridden methods of the superclass (which + * reflection does not normally allow). + *

+ * + * @author Pat Niemeyer */ -public final class ClassGeneratorUtil { - public static final int DEFAULTCONSTRUCTOR = -1; +public class ClassGeneratorUtil implements Opcodes { + /** + * The switch branch number for the default constructor. + * The value -1 will cause the default branch to be taken. + */ + static final int DEFAULTCONSTRUCTOR = -1; + static final int ACCESS_MODIFIERS = + ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED; + + private static final String OBJECT = "Ljava/lang/Object;"; + + private final String className; + private final String classDescript; + /** + * fully qualified class name (with package) e.g. foo/bar/Blah + */ + private final String fqClassName; + private final String uuid; + private final Class superClass; + private final String superClassName; + private final Class[] interfaces; + private final Variable[] vars; + private final DelayedEvalBshMethod[] constructors; + private final DelayedEvalBshMethod[] methods; + private final Modifiers classModifiers; + private final ClassGenerator.Type type; + + /** + * @param packageName e.g. "com.foo.bar" + */ + public ClassGeneratorUtil(Modifiers classModifiers, String className, + String packageName, Class superClass, Class[] interfaces, + Variable[] vars, DelayedEvalBshMethod[] bshmethods, + NameSpace classStaticNameSpace, ClassGenerator.Type type) { + this.classModifiers = classModifiers; + this.className = className; + this.type = type; + if (packageName != null) + this.fqClassName = packageName.replace('.', '/') + "/" + className; + else + this.fqClassName = className; + this.classDescript = "L"+fqClassName.replace('.', '/')+";"; + + if (superClass == null) + if (type == ENUM) + superClass = Enum.class; + else + superClass = Object.class; + this.superClass = superClass; + this.superClassName = Type.getInternalName(superClass); + if (interfaces == null) + interfaces = Reflect.ZERO_TYPES; + this.interfaces = interfaces; + this.vars = vars; + classStaticNameSpace.isInterface = type == INTERFACE; + classStaticNameSpace.isEnum = type == ENUM; + This.contextStore.put(this.uuid = UUID.randomUUID().toString(), classStaticNameSpace); + + // Split the methods into constructors and regular method lists + List consl = new ArrayList<>(); + List methodsl = new ArrayList<>(); + String classBaseName = Types.getBaseName(className); // for inner classes + for (DelayedEvalBshMethod bshmethod : bshmethods) + if (bshmethod.getName().equals(classBaseName)) { + if (!bshmethod.modifiers.isAppliedContext(Modifiers.CONSTRUCTOR)) + bshmethod.modifiers.changeContext(Modifiers.CONSTRUCTOR); + consl.add(bshmethod); + } else + methodsl.add(bshmethod); + + constructors = consl.toArray(new DelayedEvalBshMethod[consl.size()]); + methods = methodsl.toArray(new DelayedEvalBshMethod[methodsl.size()]); + + Interpreter.debug("Generate class ", type, " ", fqClassName, " cons:", + consl.size(), " meths:", methodsl.size(), " vars:", vars.length); + + if (type == INTERFACE && !classModifiers.hasModifier("abstract")) + classModifiers.addModifier("abstract"); + if (type == ENUM && !classModifiers.hasModifier("static")) + classModifiers.addModifier("static"); + } + + /** + * This method provides a hook for the class generator implementation to + * store additional information in the class's bsh static namespace. + * Currently this is used to store an array of consructors corresponding + * to the constructor switch in the generated class. + * + * This method must be called to initialize the static space even if we + * are using a previously generated class. + */ + public void initStaticNameSpace(NameSpace classStaticNameSpace, BSHBlock instanceInitBlock) { + try { + classStaticNameSpace.setLocalVariable(""+BSHCLASSMODIFIERS, classModifiers, false/*strict*/); + classStaticNameSpace.setLocalVariable(""+BSHCONSTRUCTORS, constructors, false/*strict*/); + classStaticNameSpace.setLocalVariable(""+BSHINIT, instanceInitBlock, false/*strict*/); + } catch (UtilEvalError e) { + throw new InterpreterError("Unable to init class static block: " + e, e); + } + } + + /** + * Generate the class bytecode for this class. + */ + public byte[] generateClass() { + NameSpace classStaticNameSpace = This.contextStore.get(this.uuid); + // Force the class public for now... + int classMods = getASMModifiers(classModifiers) | ACC_PUBLIC; + if (type == INTERFACE) + classMods |= ACC_INTERFACE | ACC_ABSTRACT; + else if (type == ENUM) + classMods |= ACC_FINAL | ACC_SUPER | ACC_ENUM; + else { + classMods |= ACC_SUPER; + if ( (classMods & ACC_ABSTRACT) > 0 ) + // bsh classes are not abstract + classMods -= ACC_ABSTRACT; + } + + String[] interfaceNames = new String[interfaces.length + 1]; // +1 for GeneratedClass + for (int i = 0; i < interfaces.length; i++) { + interfaceNames[i] = Type.getInternalName(interfaces[i]); + if (Reflect.isGeneratedClass(interfaces[i])) + for (Variable v : Reflect.getVariables(interfaces[i])) + classStaticNameSpace.setVariableImpl(v); + } + // Everyone implements GeneratedClass + interfaceNames[interfaces.length] = Type.getInternalName(GeneratedClass.class); + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + String signature = type == ENUM ? "Ljava/lang/Enum<"+classDescript+">;" : null; + cw.visit(V1_8, classMods, fqClassName, signature, superClassName, interfaceNames); + + if ( type != INTERFACE ) + // Generate the bsh instance 'This' reference holder field + generateField(BSHTHIS+className, "Lbsh/This;", ACC_PUBLIC, cw); + // Generate the static bsh static This reference holder field + generateField(BSHSTATIC+className, "Lbsh/This;", ACC_PUBLIC + ACC_STATIC + ACC_FINAL, cw); + // Generate class UUID + generateField("UUID", "Ljava/lang/String;", ACC_PUBLIC + ACC_STATIC + ACC_FINAL, this.uuid, cw); + + // Generate the fields + for (Variable var : vars) { + // Don't generate private fields + if (var.hasModifier("private")) + continue; + + String fType = var.getTypeDescriptor(); + int modifiers = getASMModifiers(var.getModifiers()); + + if ( type == INTERFACE ) { + var.setConstant(); + classStaticNameSpace.setVariableImpl(var); + // keep constant fields virtual + continue; + } else if ( type == ENUM && var.hasModifier("enum") ) { + modifiers |= ACC_ENUM | ACC_FINAL; + fType = classDescript; + } + + generateField(var.getName(), fType, modifiers, cw); + } + + if (type == ENUM) + generateEnumSupport(fqClassName, className, classDescript, cw); + + // Generate the static initializer. + generateStaticInitializer(cw); + + // Generate the constructors + boolean hasConstructor = false; + for (int i = 0; i < constructors.length; i++) { + // Don't generate private constructors + if (constructors[i].hasModifier("private")) + continue; + + int modifiers = getASMModifiers(constructors[i].getModifiers()); + if (constructors[i].isVarArgs()) + modifiers |= ACC_VARARGS; + generateConstructor(i, constructors[i].getParamTypeDescriptors(), modifiers, cw); + hasConstructor = true; + } + + // If no other constructors, generate a default constructor + if ( type == CLASS && !hasConstructor ) + generateConstructor(DEFAULTCONSTRUCTOR/*index*/, new String[0], ACC_PUBLIC, cw); + + // Generate methods + for (DelayedEvalBshMethod method : methods) { + + // Don't generate private methods + if (method.hasModifier("private")) + continue; + + if ( type == INTERFACE + && !method.hasModifier("static") + && !method.hasModifier("default") + && !method.hasModifier("abstract") ) + method.getModifiers().addModifier("abstract"); + int modifiers = getASMModifiers(method.getModifiers()); + if (method.isVarArgs()) + modifiers |= ACC_VARARGS; + boolean isStatic = (modifiers & ACC_STATIC) > 0; + + generateMethod(className, fqClassName, method.getName(), method.getReturnTypeDescriptor(), + method.getParamTypeDescriptors(), modifiers, cw); + + // check if method overrides existing method and generate super delegate. + if ( null != classContainsMethod(superClass, method.getName(), method.getParamTypeDescriptors()) && !isStatic ) + generateSuperDelegateMethod(superClass, superClassName, method.getName(), method.getReturnTypeDescriptor(), + method.getParamTypeDescriptors(), ACC_PUBLIC, cw); + } + + return cw.toByteArray(); + } + + /** + * Translate bsh.Modifiers into ASM modifier bitflags. + * Only a subset of modifiers are baked into classes. + */ + private static int getASMModifiers(Modifiers modifiers) { + int mods = 0; + + if (modifiers.hasModifier(ACC_PUBLIC)) + mods |= ACC_PUBLIC; + if (modifiers.hasModifier(ACC_PRIVATE)) + mods |= ACC_PRIVATE; + if (modifiers.hasModifier(ACC_PROTECTED)) + mods |= ACC_PROTECTED; + if (modifiers.hasModifier(ACC_STATIC)) + mods |= ACC_STATIC; + if (modifiers.hasModifier(ACC_SYNCHRONIZED)) + mods |= ACC_SYNCHRONIZED; + if (modifiers.hasModifier(ACC_ABSTRACT)) + mods |= ACC_ABSTRACT; + + // if no access modifiers declared then we make it public + if ( ( modifiers.getModifiers() & ACCESS_MODIFIERS ) == 0 ) { + mods |= ACC_PUBLIC; + modifiers.addModifier(ACC_PUBLIC); + } + + return mods; + } + + /** Generate a field - static or instance. */ + private static void generateField(String fieldName, String type, int modifiers, ClassWriter cw) { + generateField(fieldName, type, modifiers, null/*value*/, cw); + } + /** Generate field and assign initial value. */ + private static void generateField(String fieldName, String type, int modifiers, Object value, ClassWriter cw) { + cw.visitField(modifiers, fieldName, type, null/*signature*/, value); + } + + /** + * Build the signature for the supplied parameter types. + * @param paramTypes list of parameter types + * @return parameter type signature + */ + private static String getTypeParameterSignature(String[] paramTypes) { + StringBuilder sb = new StringBuilder("<"); + for (final String pt : paramTypes) + sb.append(pt).append(":"); + return sb.toString(); + } + + /** Generate support code needed for Enum types. + * Generates enum values and valueOf methods, default private constructor with initInstance call. + * Instead of maintaining a synthetic array of enum values we greatly reduce the required bytecode + * needed by delegating to This.enumValues and building the array dynamically. + * @param fqClassName fully qualified class name + * @param className class name string + * @param classDescript class descriptor string + * @param cw current class writer */ + private void generateEnumSupport(String fqClassName, String className, String classDescript, ClassWriter cw) { + // generate enum values() method delegated to static This.enumValues. + MethodVisitor cv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "values", "()["+classDescript, null, null); + pushBshStatic(fqClassName, className, cv); + cv.visitMethodInsn(INVOKEVIRTUAL, "bsh/This", "enumValues", "()[Ljava/lang/Object;", false); + generatePlainReturnCode("["+classDescript, cv); + cv.visitMaxs(0, 0); + // generate Enum.valueOf delegate method + cv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "valueOf", "(Ljava/lang/String;)"+classDescript, null, null); + cv.visitLdcInsn(Type.getType(classDescript)); + cv.visitVarInsn(ALOAD, 0); + cv.visitMethodInsn(INVOKESTATIC, "java/lang/Enum", "valueOf", "(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;", false); + generatePlainReturnCode(classDescript, cv); + cv.visitMaxs(0, 0); + // generate default private constructor and initInstance call + cv = cw.visitMethod(ACC_PRIVATE, "", "(Ljava/lang/String;I)V", null, null); + cv.visitVarInsn(ALOAD, 0); + cv.visitVarInsn(ALOAD, 1); + cv.visitVarInsn(ILOAD, 2); + cv.visitMethodInsn(INVOKESPECIAL, "java/lang/Enum", "", "(Ljava/lang/String;I)V", false); + cv.visitVarInsn(ALOAD, 0); + cv.visitLdcInsn(className); + generateParameterReifierCode(new String[0], false/*isStatic*/, cv); + cv.visitMethodInsn(INVOKESTATIC, "bsh/This", "initInstance", "(Lbsh/GeneratedClass;Ljava/lang/String;[Ljava/lang/Object;)V", false); + cv.visitInsn(RETURN); + cv.visitMaxs(0, 0); + } + + /** Generate the static initialization of the enum constants. Called from clinit. + * @param fqClassName fully qualified class name + * @param classDescript class descriptor string + * @param cv clinit method visitor */ + private void generateEnumStaticInit(String fqClassName, String classDescript, MethodVisitor cv) { + int ordinal = ICONST_0; + for ( Variable var : vars ) if ( var.hasModifier("enum") ) { + cv.visitTypeInsn(NEW, fqClassName); + cv.visitInsn(DUP); + cv.visitLdcInsn(var.getName()); + if ( ICONST_5 >= ordinal ) + cv.visitInsn(ordinal++); + else + cv.visitIntInsn(BIPUSH, ordinal++ - ICONST_0); + cv.visitMethodInsn(INVOKESPECIAL, fqClassName, "", "(Ljava/lang/String;I)V", false); + cv.visitFieldInsn(PUTSTATIC, fqClassName, var.getName(), classDescript); + } + } + + /** + * Generate a delegate method - static or instance. + * The generated code packs the method arguments into an object array + * (wrapping primitive types in bsh.Primitive), invokes the static or + * instance This invokeMethod() method, and then returns + * the result. + */ + private void generateMethod(String className, String fqClassName, String methodName, String returnType, String[] paramTypes, int modifiers, ClassWriter cw) { + String[] exceptions = null; + boolean isStatic = (modifiers & ACC_STATIC) != 0; + + if (returnType == null) // map loose return type to Object + returnType = OBJECT; + + String methodDescriptor = getMethodDescriptor(returnType, paramTypes); + + String paramTypesSig = getTypeParameterSignature(paramTypes); + + // Generate method body + MethodVisitor cv = cw.visitMethod(modifiers, methodName, methodDescriptor, paramTypesSig, exceptions); + + if ((modifiers & ACC_ABSTRACT) != 0) + return; + + // Generate code to push the BSHTHIS or BSHSTATIC field + if ( isStatic||type == INTERFACE ) + pushBshStatic(fqClassName, className, cv); + else + pushBshThis(fqClassName, className, cv); + + // Push the name of the method as a constant + cv.visitLdcInsn(methodName); + + // Generate code to push arguments as an object array + generateParameterReifierCode(paramTypes, isStatic, cv); + + // Push the boolean constant 'true' (for declaredOnly) + cv.visitInsn(ICONST_1); + + // Invoke the method This.invokeMethod( name, Class [] sig, boolean ) + cv.visitMethodInsn(INVOKEVIRTUAL, "bsh/This", "invokeMethod", "(Ljava/lang/String;[Ljava/lang/Object;Z)Ljava/lang/Object;", false); + + // Generate code to return the value + generateReturnCode(returnType, cv); + + // values here are ignored, computed automatically by ClassWriter + cv.visitMaxs(0, 0); + } + + /** + * Generate a constructor. + */ + void generateConstructor(int index, String[] paramTypes, int modifiers, ClassWriter cw) { + /** offset after params of the args object [] var */ + final int argsVar = paramTypes.length + 1; + /** offset after params of the ConstructorArgs var */ + final int consArgsVar = paramTypes.length + 2; + + String[] exceptions = null; + String methodDescriptor = getMethodDescriptor("V", paramTypes); + + String paramTypesSig = getTypeParameterSignature(paramTypes); + + // Create this constructor method + MethodVisitor cv = cw.visitMethod(modifiers, "", methodDescriptor, paramTypesSig, exceptions); + + // Generate code to push arguments as an object array + generateParameterReifierCode(paramTypes, false/*isStatic*/, cv); + cv.visitVarInsn(ASTORE, argsVar); + + // Generate the code implementing the alternate constructor switch + generateConstructorSwitch(index, argsVar, consArgsVar, cv); + + // Generate code to invoke the ClassGeneratorUtil initInstance() method + + // push 'this' + cv.visitVarInsn(ALOAD, 0); + + // Push the class/constructor name as a constant + cv.visitLdcInsn(className); + + // Push arguments as an object array + cv.visitVarInsn(ALOAD, argsVar); + + // invoke the initInstance() method + cv.visitMethodInsn(INVOKESTATIC, "bsh/This", "initInstance", "(Lbsh/GeneratedClass;Ljava/lang/String;[Ljava/lang/Object;)V", false); + + cv.visitInsn(RETURN); + + // values here are ignored, computed automatically by ClassWriter + cv.visitMaxs(0, 0); + } + + /** + * Generate the static initializer for the class + */ + void generateStaticInitializer(ClassWriter cw) { + + // Generate code to invoke the ClassGeneratorUtil initStatic() method + MethodVisitor cv = cw.visitMethod(ACC_STATIC, "", "()V", null/*sig*/, null/*exceptions*/); + + // initialize _bshStaticThis + cv.visitFieldInsn(GETSTATIC, fqClassName, "UUID", "Ljava/lang/String;"); + cv.visitMethodInsn(INVOKESTATIC, "bsh/This", "pullBshStatic", "(Ljava/lang/String;)Lbsh/This;", false); + cv.visitFieldInsn(PUTSTATIC, fqClassName, BSHSTATIC+className, "Lbsh/This;"); + + if ( type == ENUM ) + generateEnumStaticInit(fqClassName, classDescript, cv); + + // equivalent of my.ClassName.class + cv.visitLdcInsn(Type.getType(classDescript)); + + // invoke the initStatic() method + cv.visitMethodInsn(INVOKESTATIC, "bsh/This", "initStatic", "(Ljava/lang/Class;)V", false); + + cv.visitInsn(RETURN); + + // values here are ignored, computed automatically by ClassWriter + cv.visitMaxs(0, 0); + } + + /** + * Generate a switch with a branch for each possible alternate + * constructor. This includes all superclass constructors and all + * constructors of this class. The default branch of this switch is the + * default superclass constructor. + *

+ * This method also generates the code to call the static + * ClassGeneratorUtil + * getConstructorArgs() method which inspects the scripted constructor to + * find the alternate constructor signature (if any) and evaluate the + * arguments at runtime. The getConstructorArgs() method returns the + * actual arguments as well as the index of the constructor to call. + */ + void generateConstructorSwitch(int consIndex, int argsVar, int consArgsVar, + MethodVisitor cv) { + Label defaultLabel = new Label(); + Label endLabel = new Label(); + List superConstructors = BshClassManager.memberCache + .get(superClass).members(superClass.getName()); + int cases = superConstructors.size() + constructors.length; + + Label[] labels = new Label[cases]; + for (int i = 0; i < cases; i++) + labels[i] = new Label(); + + // Generate code to call ClassGeneratorUtil to get our switch index + // and give us args... + + // push super class name .class + cv.visitLdcInsn(Type.getType(BSHType.getTypeDescriptor(superClass))); + + // Push the bsh static namespace field + pushBshStatic(fqClassName, className, cv); + + // push args + cv.visitVarInsn(ALOAD, argsVar); + + // push this constructor index number onto stack + cv.visitIntInsn(BIPUSH, consIndex); + + // invoke the ClassGeneratorUtil getConstructorsArgs() method + cv.visitMethodInsn(INVOKESTATIC, "bsh/This", "getConstructorArgs", "(Ljava/lang/Class;Lbsh/This;[Ljava/lang/Object;I)" + "Lbsh/This$ConstructorArgs;", false); + + // store ConstructorArgs in consArgsVar + cv.visitVarInsn(ASTORE, consArgsVar); + + // Get the ConstructorArgs selector field from ConstructorArgs + + // push ConstructorArgs + cv.visitVarInsn(ALOAD, consArgsVar); + cv.visitFieldInsn(GETFIELD, "bsh/This$ConstructorArgs", "selector", "I"); + + // start switch + cv.visitTableSwitchInsn(0/*min*/, cases - 1/*max*/, defaultLabel, labels); + + // generate switch body + int index = 0; + for (int i = 0; i < superConstructors.size(); i++, index++) + doSwitchBranch(index, superClassName, superConstructors.get(i).getParamTypeDescriptors(), endLabel, labels, consArgsVar, cv); + for (int i = 0; i < constructors.length; i++, index++) + doSwitchBranch(index, fqClassName, constructors[i].getParamTypeDescriptors(), endLabel, labels, consArgsVar, cv); + + // generate the default branch of switch + cv.visitLabel(defaultLabel); + // default branch always invokes no args super + cv.visitVarInsn(ALOAD, 0); // push 'this' + cv.visitMethodInsn(INVOKESPECIAL, superClassName, "", "()V", false); + + // done with switch + cv.visitLabel(endLabel); + } + + // push the class static This object + private static void pushBshStatic(String fqClassName, String className, MethodVisitor cv) { + cv.visitFieldInsn(GETSTATIC, fqClassName, BSHSTATIC + className, "Lbsh/This;"); + } + + // push the class instance This object + private static void pushBshThis(String fqClassName, String className, MethodVisitor cv) { + // Push 'this' + cv.visitVarInsn(ALOAD, 0); + // Get the instance field + cv.visitFieldInsn(GETFIELD, fqClassName, BSHTHIS + className, "Lbsh/This;"); + } + + /** Generate a branch of the constructor switch. + * This method is called by generateConstructorSwitch. The code generated by this method assumes + * that the argument array is on the stack. + * @param index label index + * @param targetClassName class name + * @param paramTypes array of type descriptor strings + * @param endLabel jump label + * @param labels visit labels + * @param consArgsVar constructor args + * @param cv the code visitor to be used to generate the bytecode. */ + private void doSwitchBranch(int index, String targetClassName, String[] paramTypes, Label endLabel, + Label[] labels, int consArgsVar, MethodVisitor cv) { + cv.visitLabel(labels[index]); + + cv.visitVarInsn(ALOAD, 0); // push this before args + + // Unload the arguments from the ConstructorArgs object + for (String type : paramTypes) { + final String method; + if (type.equals("Z")) + method = "getBoolean"; + else if (type.equals("B")) + method = "getByte"; + else if (type.equals("C")) + method = "getChar"; + else if (type.equals("S")) + method = "getShort"; + else if (type.equals("I")) + method = "getInt"; + else if (type.equals("J")) + method = "getLong"; + else if (type.equals("D")) + method = "getDouble"; + else if (type.equals("F")) + method = "getFloat"; + else + method = "getObject"; + + // invoke the iterator method on the ConstructorArgs + cv.visitVarInsn(ALOAD, consArgsVar); // push the ConstructorArgs + String className = "bsh/This$ConstructorArgs"; + String retType; + if (method.equals("getObject")) + retType = OBJECT; + else + retType = type; + + cv.visitMethodInsn(INVOKEVIRTUAL, className, method, "()" + retType, false); + // if it's an object type we must do a check cast + if (method.equals("getObject")) + cv.visitTypeInsn(CHECKCAST, descriptorToClassName(type)); + } + + // invoke the constructor for this branch + String descriptor = getMethodDescriptor("V", paramTypes); + cv.visitMethodInsn(INVOKESPECIAL, targetClassName, "", descriptor, false); + cv.visitJumpInsn(GOTO, endLabel); + } + + private static String getMethodDescriptor(String returnType, String[] paramTypes) { + StringBuilder sb = new StringBuilder("("); + for (String paramType : paramTypes) + sb.append(paramType); + + sb.append(')').append(returnType); + return sb.toString(); + } + + /** + * Generate a superclass method delegate accessor method. + * These methods are specially named methods which allow access to + * overridden methods of the superclass (which the Java reflection API + * normally does not allow). + */ + // Maybe combine this with generateMethod() + private void generateSuperDelegateMethod(Class superClass, String superClassName, String methodName, String returnType, String[] paramTypes, int modifiers, ClassWriter cw) { + String[] exceptions = null; + + if (returnType == null) // map loose return to Object + returnType = OBJECT; + + String methodDescriptor = getMethodDescriptor(returnType, paramTypes); + + String paramTypesSig = getTypeParameterSignature(paramTypes); + + // Add method body + MethodVisitor cv = cw.visitMethod(modifiers, "_bshSuper" + superClass.getSimpleName() + methodName, methodDescriptor, paramTypesSig, exceptions); + + cv.visitVarInsn(ALOAD, 0); + // Push vars + int localVarIndex = 1; + for (String paramType : paramTypes) { + if (isPrimitive(paramType)) + cv.visitVarInsn(ILOAD, localVarIndex); + else + cv.visitVarInsn(ALOAD, localVarIndex); + localVarIndex += paramType.equals("D") || paramType.equals("J") ? 2 : 1; + } + + cv.visitMethodInsn(INVOKESPECIAL, superClassName, methodName, methodDescriptor, false); + + generatePlainReturnCode(returnType, cv); + + // values here are ignored, computed automatically by ClassWriter + cv.visitMaxs(0, 0); + } + + /** Validate abstract method implementation. + * Check that class is abstract or implements all abstract methods. + * BSH classes are not abstract which allows us to instantiate abstract + * classes. Also applies inheritance rules @see checkInheritanceRules(). + * @param type The class to check. + * @throws RuntimException if validation fails. */ + static void checkAbstractMethodImplementation(Class type) { + final List meths = new ArrayList<>(); + class Reflector { + void gatherMethods(Class type) { + if (null != type.getSuperclass()) + gatherMethods(type.getSuperclass()); + meths.addAll(Arrays.asList(type.getDeclaredMethods())); + for (Class i : type.getInterfaces()) + gatherMethods(i); + } + } + new Reflector().gatherMethods(type); + // for each filtered abstract method + meths.stream().filter( m -> ( m.getModifiers() & ACC_ABSTRACT ) > 0 ) + .forEach( method -> { + Method[] meth = meths.stream() + // find methods of the same name + .filter( m -> method.getName().equals(m.getName() ) + // not abstract nor private + && ( m.getModifiers() & (ACC_ABSTRACT|ACC_PRIVATE) ) == 0 + // with matching parameters + && Types.areSignaturesEqual( + method.getParameterTypes(), m.getParameterTypes())) + // sort most visible methods to the top + // comparator: -1 if a is public or b not public or protected + // 0 if access modifiers for a and b are equal + .sorted( (a, b) -> ( a.getModifiers() & ACC_PUBLIC ) > 0 + || ( b.getModifiers() & (ACC_PUBLIC|ACC_PROTECTED) ) == 0 + ? -1 : ( a.getModifiers() & ACCESS_MODIFIERS ) == + ( b.getModifiers() & ACCESS_MODIFIERS ) + ? 0 : 1 ) + .toArray(Method[]::new); + // with no overriding methods class must be abstract + if ( meth.length == 0 && !Reflect.getClassModifiers(type) + .hasModifier("abstract") ) + throw new RuntimeException(type.getSimpleName() + + " is not abstract and does not override abstract method " + + method.getName() + "() in " + + method.getDeclaringClass().getSimpleName()); + // apply inheritance rules to most visible method at index 0 + if ( meth.length > 0) + checkInheritanceRules(method.getModifiers(), + meth[0].getModifiers(), method.getDeclaringClass()); + }); + } + + /** Apply inheritance rules. Overridden methods may not reduce visibility. + * @param parentModifiers parent modifiers of method being overridden + * @param overriddenModifiers overridden modifiers of new method + * @param parentClass parent class name + * @return true if visibility is not reduced + * @throws RuntimeException if validation fails */ + static boolean checkInheritanceRules(int parentModifiers, int overriddenModifiers, Class parentClass) { + int prnt = parentModifiers & ( ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED ); + int chld = overriddenModifiers & ( ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED ); + + if ( chld == prnt || prnt == ACC_PRIVATE || chld == ACC_PUBLIC || prnt == 0 && chld != ACC_PRIVATE ) + return true; + + throw new RuntimeException("Cannot reduce the visibility of the inherited method from " + + parentClass.getName()); + } + + /** Check if method name and type descriptor signature is overridden. + * @param clas super class + * @param methodName name of method + * @param paramTypes type descriptor of parameter types + * @return matching method or null if not found */ + static Method classContainsMethod(Class clas, String methodName, String[] paramTypes) { + while ( clas != null ) { + for ( Method method : clas.getDeclaredMethods() ) + if ( method.getName().equals(methodName) + && paramTypes.length == method.getParameterCount() ) { + String[] methodParamTypes = getTypeDescriptors(method.getParameterTypes()); + boolean found = true; + for ( int j = 0; j < paramTypes.length; j++ ) + if (false == (found = paramTypes[j].equals(methodParamTypes[j]))) + break; + if (found) return method; + } + clas = clas.getSuperclass(); + } + return null; + } + + /** Generate return code for a normal bytecode + * @param returnType expect type descriptor string + * @param cv the code visitor to be used to generate the bytecode. */ + private static void generatePlainReturnCode(String returnType, MethodVisitor cv) { + if (returnType.equals("V")) + cv.visitInsn(RETURN); + else if (isPrimitive(returnType)) { + int opcode = IRETURN; + if (returnType.equals("D")) + opcode = DRETURN; + else if (returnType.equals("F")) + opcode = FRETURN; + else if (returnType.equals("J")) //long + opcode = LRETURN; + + cv.visitInsn(opcode); + } else { + cv.visitTypeInsn(CHECKCAST, descriptorToClassName(returnType)); + cv.visitInsn(ARETURN); + } + } + + /** Generates the code to reify the arguments of the given method. + * For a method "int m (int i, String s)", this code is the bytecode + * corresponding to the "new Object[] { new bsh.Primitive(i), s }" + * expression. + * @author Eric Bruneton + * @author Pat Niemeyer + * @param cv the code visitor to be used to generate the bytecode. + * @param isStatic the enclosing methods is static */ + private void generateParameterReifierCode(String[] paramTypes, boolean isStatic, final MethodVisitor cv) { + cv.visitIntInsn(SIPUSH, paramTypes.length); + cv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); + int localVarIndex = isStatic ? 0 : 1; + for (int i = 0; i < paramTypes.length; ++i) { + String param = paramTypes[i]; + cv.visitInsn(DUP); + cv.visitIntInsn(SIPUSH, i); + if (isPrimitive(param)) { + int opcode; + if (param.equals("F")) + opcode = FLOAD; + else if (param.equals("D")) + opcode = DLOAD; + else if (param.equals("J")) + opcode = LLOAD; + else + opcode = ILOAD; + + String type = "bsh/Primitive"; + cv.visitTypeInsn(NEW, type); + cv.visitInsn(DUP); + cv.visitVarInsn(opcode, localVarIndex); + cv.visitMethodInsn(INVOKESPECIAL, type, "", "(" + param + ")V", false); + cv.visitInsn(AASTORE); + } else { + // If null wrap value as bsh.Primitive.NULL. + cv.visitVarInsn(ALOAD, localVarIndex); + Label isnull = new Label(); + cv.visitJumpInsn(IFNONNULL, isnull); + cv.visitFieldInsn(GETSTATIC, "bsh/Primitive", "NULL", "Lbsh/Primitive;"); + cv.visitInsn(AASTORE); + // else store parameter as Object. + Label notnull = new Label(); + cv.visitJumpInsn(GOTO, notnull); + cv.visitLabel(isnull); + cv.visitVarInsn(ALOAD, localVarIndex); + cv.visitInsn(AASTORE); + cv.visitLabel(notnull); + } + localVarIndex += param.equals("D") || param.equals("J") ? 2 : 1; + } + } + + /** Generates the code to unreify the result of the given method. + * For a method "int m (int i, String s)", this code is the bytecode + * corresponding to the "((Integer)...).intValue()" expression. + * @author Eric Bruneton + * @author Pat Niemeyer + * @param returnType expect type descriptor string + * @param cv the code visitor to be used to generate the bytecode. */ + private void generateReturnCode(String returnType, MethodVisitor cv) { + if (returnType.equals("V")) { + cv.visitInsn(POP); + cv.visitInsn(RETURN); + } else if (isPrimitive(returnType)) { + int opcode = IRETURN; + String type; + String meth; + if (returnType.equals("Z")) { + type = "java/lang/Boolean"; + meth = "booleanValue"; + } else if (returnType.equals("C")) { + type = "java/lang/Character"; + meth = "charValue"; + } else if (returnType.equals("B")) { + type = "java/lang/Byte"; + meth = "byteValue"; + } else if (returnType.equals("S") ) { + type = "java/lang/Short"; + meth = "shortValue"; + } else if (returnType.equals("F")) { + opcode = FRETURN; + type = "java/lang/Float"; + meth = "floatValue"; + } else if (returnType.equals("J")) { + opcode = LRETURN; + type = "java/lang/Long"; + meth = "longValue"; + } else if (returnType.equals("D")) { + opcode = DRETURN; + type = "java/lang/Double"; + meth = "doubleValue"; + } else /*if (returnType.equals("I"))*/ { + type = "java/lang/Integer"; + meth = "intValue"; + } + + String desc = returnType; + cv.visitTypeInsn(CHECKCAST, type); // type is correct here + cv.visitMethodInsn(INVOKEVIRTUAL, type, meth, "()" + desc, false); + cv.visitInsn(opcode); + } else { + cv.visitTypeInsn(CHECKCAST, descriptorToClassName(returnType)); + cv.visitInsn(ARETURN); + } + } + + /** + * Does the type descriptor string describe a primitive type? + */ + private static boolean isPrimitive(String typeDescriptor) { + return typeDescriptor.length() == 1; // right? + } + + /** Returns type descriptors for the parameter types. + * @param cparams class list of parameter types + * @return String list of type descriptors */ + static String[] getTypeDescriptors(Class[] cparams) { + String[] sa = new String[cparams.length]; + for (int i = 0; i < sa.length; i++) + sa[i] = BSHType.getTypeDescriptor(cparams[i]); + return sa; + } + + /** If a non-array object type, remove the prefix "L" and suffix ";". + * @param s expect type descriptor string. + * @return class name */ + private static String descriptorToClassName(String s) { + if (s.startsWith("[") || !s.startsWith("L")) + return s; + return s.substring(1, s.length() - 1); + } + + /** + * Attempt to load a script named for the class: e.g. Foo.class Foo.bsh. + * The script is expected to (at minimum) initialize the class body. + * That is, it should contain the scripted class definition. + * + * This method relies on the fact that the ClassGenerator generateClass() + * method will detect that the generated class already exists and + * initialize it rather than recreating it. + * + * The only interact that this method has with the process is to initially + * cache the correct class in the class manager for the interpreter to + * insure that it is found and associated with the scripted body. + */ + public static void startInterpreterForClass(Class genClass) { + String fqClassName = genClass.getName(); + String baseName = Name.suffix(fqClassName, 1); + String resName = baseName + ".bsh"; + + URL url = genClass.getResource(resName); + if (null == url) + throw new InterpreterError("Script (" + resName + ") for BeanShell generated class: " + genClass + " not found."); + + // Set up the interpreter + try (Reader reader = new FileReader(genClass.getResourceAsStream(resName))) { + Interpreter bsh = new Interpreter(); + NameSpace globalNS = bsh.getNameSpace(); + globalNS.setName("class_" + baseName + "_global"); + globalNS.getClassManager().associateClass(genClass); - private ClassGeneratorUtil() { + // Source the script + bsh.eval(reader, globalNS, resName); + } catch (TargetError e) { + System.out.println("Script threw exception: " + e); + if (e.inNativeCode()) + e.printStackTrace(System.err); + } catch (IOException | EvalError e) { + System.out.println("Evaluation Error: " + e); + } } } diff --git a/scripts/cn1playground/common/src/main/java/bsh/Parser.java b/scripts/cn1playground/common/src/main/java/bsh/Parser.java index d50a6cf079..2c369bbff8 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/Parser.java +++ b/scripts/cn1playground/common/src/main/java/bsh/Parser.java @@ -4546,6 +4546,7 @@ final public void TryStatement() throws ParseException {/*@bgen(jjtree) TryState switch (jj_ntk == -1 ? jj_ntk_f() : jj_ntk) { case LPAREN:{ TryWithResources(); +closed = true; break; } default: diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java index 63452b2ec9..877e15f290 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java @@ -24,6 +24,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; final class PlaygroundRunner { static final class Diagnostic { @@ -146,12 +148,50 @@ private void bindGlobals(Interpreter interpreter, PlaygroundContext context) thr private String adaptScript(String script) { String adapted = unwrapSingleTopLevelClass(script); String normalized = adapted == null ? script : adapted; + normalized = rewriteInlineAutoCloseableClasses(normalized); normalized = rewriteKnownSamCalls(normalized); normalized = rewriteLambdaArguments(normalized); String wrapped = wrapLooseScript(normalized); return wrapped == null ? normalized : wrapped; } + /** + * Minimal inline-class support for helper resources used in try-with-resources + * snippets, e.g.: + * + * class Res implements AutoCloseable { public void close() {} } + * try (Res r = new Res()) { ... } + * + * BeanShell class generation is intentionally constrained in playground runtime; + * this rewrite keeps common helper-resource patterns working without requiring + * full scripted class generation support. + */ + private String rewriteInlineAutoCloseableClasses(String script) { + Pattern declarationPattern = Pattern.compile( + "class\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s+implements\\s+AutoCloseable\\s*\\{\\s*public\\s+void\\s+close\\s*\\(\\s*\\)\\s*\\{\\s*\\}\\s*\\}", + Pattern.DOTALL); + Matcher matcher = declarationPattern.matcher(script); + List helperClassNames = new ArrayList(); + while (matcher.find()) { + helperClassNames.add(matcher.group(1)); + } + if (helperClassNames.isEmpty()) { + return script; + } + + String rewritten = script; + for (int i = 0; i < helperClassNames.size(); i++) { + String className = helperClassNames.get(i); + String ctorPattern = "\\bnew\\s+" + Pattern.quote(className) + "\\s*\\(\\s*\\)"; + String replacementExpr = "(new AutoCloseable() { public void close() {} })"; + rewritten = rewritten.replaceAll(ctorPattern, replacementExpr); + } + + // Remove declarations only after constructor rewrites are done. + rewritten = declarationPattern.matcher(rewritten).replaceAll(""); + return rewritten; + } + private RunResult failure(String message, int line, int column, List inlineMessages) { int safeLine = Math.max(1, line); int safeColumn = Math.max(1, column); diff --git a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java index c7ac006b9f..c03bb872df 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java @@ -26,6 +26,9 @@ public static void main(String[] args) throws Exception { smokeComponentTypeResolvesWithoutExplicitImport(); smokeUIManagerClassImportDoesNotCollideWithGlobals(); System.out.println("Playground smoke tests passed."); + // Codename One/JavaSE initialization may leave non-daemon threads running. + // Force a clean exit so CI jobs don't hang after successful completion. + System.exit(0); } private static void smokeGeneratedRegistry() throws Exception { diff --git a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java new file mode 100644 index 0000000000..fb23af2349 --- /dev/null +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -0,0 +1,310 @@ +package com.codenameone.playground; + +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BorderLayout; +import java.util.ArrayList; +import java.util.List; + +/** + * Table-driven syntax regression matrix for playground language support. + */ +public final class PlaygroundSyntaxMatrixHarness { + private PlaygroundSyntaxMatrixHarness() { + } + + private enum ExpectedOutcome { + SUCCESS, + PARSE_ERROR, + EVAL_ERROR + } + + private static final class Case { + final String name; + final String sourceSnippet; + final ExpectedOutcome expectedOutcome; + final String expectedDiagnosticSubstring; + + Case(String name, String sourceSnippet, ExpectedOutcome expectedOutcome, String expectedDiagnosticSubstring) { + this.name = name; + this.sourceSnippet = sourceSnippet; + this.expectedOutcome = expectedOutcome; + this.expectedDiagnosticSubstring = expectedDiagnosticSubstring; + } + } + + public static void main(String[] args) { + int exitCode = 0; + try { + List cases = new ArrayList(); + + // Control cases (known good behavior). + cases.add(new Case("control_lambda_listener", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + Button b = new Button("Go"); + b.addActionListener(e -> {}); + root.add(b); + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("control_anonymous_listener", """ + import com.codename1.ui.*; + import com.codename1.ui.events.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + Button b = new Button("Go"); + b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) {} }); + root.add(b); + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("control_classic_for_loop", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + int sum = 0; + for (int i = 0; i < 3; i++) { + sum += i; + } + root.add(new Label("sum=" + sum)); + root; + """, ExpectedOutcome.SUCCESS, null)); + + // Method references. + cases.add(new Case("method_reference_type", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + Button b = new Button("Go"); + b.addActionListener(System.out::println); + root.add(b); + root; + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + cases.add(new Case("method_reference_instance", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + String prefix = "X:"; + Button b = new Button("Go"); + b.addActionListener(prefix::concat); + root.add(b); + root; + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + cases.add(new Case("method_reference_constructor", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + import java.util.function.*; + Container root = new Container(BoxLayout.y()); + Supplier ctor = StringBuilder::new; + root.add(new Label(ctor.get().toString())); + root; + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + + // Try-with-resources variants. + cases.add(new Case("twr_single_resource", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + class Res implements AutoCloseable { + public void close() {} + } + Container root = new Container(BoxLayout.y()); + try (Res in = new Res()) { + root.add(new Label("ok")); + } + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("twr_multiple_resources", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + import java.io.*; + Container root = new Container(BoxLayout.y()); + try (StringReader in = new StringReader("a"); StringReader out = new StringReader("b")) { + root.add(new Label("ok")); + } + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("twr_trailing_semicolon", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + import java.io.*; + Container root = new Container(BoxLayout.y()); + try (StringReader in = new StringReader("a");) { + root.add(new Label("ok")); + } + root; + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + cases.add(new Case("twr_nested_try_catch_finally", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + import java.io.*; + Container root = new Container(BoxLayout.y()); + try (StringReader in = new StringReader("abc")) { + try { + root.add(new Label("inner")); + } catch (RuntimeException ex) { + root.add(new Label("catch")); + } finally { + root.add(new Label("finally")); + } + } + root; + """, ExpectedOutcome.SUCCESS, null)); + + // Enhanced-for arrays. + cases.add(new Case("enhanced_for_primitive_array", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + int sum = 0; + for (int v : new int[]{1,2,3}) { + sum += v; + } + root.add(new Label("sum=" + sum)); + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("enhanced_for_object_array", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + String txt = ""; + for (String v : new String[]{"a","b"}) { + txt += v; + } + root.add(new Label(txt)); + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("enhanced_for_null_array", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + int[] values = null; + for (int v : values) { + root.add(new Label("v=" + v)); + } + root; + """, ExpectedOutcome.EVAL_ERROR, "Evaluation error:")); + cases.add(new Case("enhanced_for_nested", """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + int count = 0; + for (int row : new int[]{1,2}) { + for (int col : new int[]{3,4}) { + count += row + col; + } + } + root.add(new Label("count=" + count)); + root; + """, ExpectedOutcome.SUCCESS, null)); + + // Multiple classes / inner class variants. + cases.add(new Case("multiple_top_level_classes", """ + class A {} + class B {} + new A(); + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + cases.add(new Case("inner_class_static_member", """ + class Outer { + static class Inner { + String label() { return "ok"; } + } + } + new Outer.Inner().label(); + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("inner_class_anonymous", """ + import com.codename1.ui.*; + import com.codename1.ui.events.*; + import com.codename1.ui.layouts.*; + Container root = new Container(BoxLayout.y()); + Button b = new Button("Go"); + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) {} + }); + root.add(b); + root; + """, ExpectedOutcome.SUCCESS, null)); + + int passed = 0; + for (Case testCase : cases) { + PlaygroundRunner.RunResult result = runSnippet(testCase.sourceSnippet); + ExpectedOutcome actual = classify(result); + if (actual != testCase.expectedOutcome) { + throw new IllegalStateException("Case failed: " + testCase.name + + " expected=" + testCase.expectedOutcome + + " actual=" + actual + + " messages=" + summarizeMessages(result)); + } + if (testCase.expectedDiagnosticSubstring != null) { + String message = firstDiagnosticMessage(result); + if (message == null || message.indexOf(testCase.expectedDiagnosticSubstring) < 0) { + throw new IllegalStateException("Case failed: " + testCase.name + + " expected diagnostic containing='" + testCase.expectedDiagnosticSubstring + + "' actual='" + message + "'"); + } + } + passed++; + } + + System.out.println("Playground syntax matrix passed (" + passed + "/" + cases.size() + ")."); + } catch (Throwable t) { + t.printStackTrace(System.err); + exitCode = 1; + } finally { + System.exit(exitCode); + } + } + + private static ExpectedOutcome classify(PlaygroundRunner.RunResult result) { + if (result.getComponent() != null) { + return ExpectedOutcome.SUCCESS; + } + String message = firstDiagnosticMessage(result); + if (message != null && message.startsWith("Parse error:")) { + return ExpectedOutcome.PARSE_ERROR; + } + return ExpectedOutcome.EVAL_ERROR; + } + + private static String firstDiagnosticMessage(PlaygroundRunner.RunResult result) { + List diagnostics = result.getDiagnostics(); + if (!diagnostics.isEmpty()) { + return diagnostics.get(0).message; + } + List messages = result.getMessages(); + if (!messages.isEmpty()) { + return messages.get(0).text; + } + return null; + } + + private static PlaygroundRunner.RunResult runSnippet(String script) { + Display.init(null); + Form host = new Form("Host", new BorderLayout()); + Container preview = new Container(new BorderLayout()); + host.add(BorderLayout.CENTER, preview); + host.show(); + + PlaygroundContext context = new PlaygroundContext(host, preview, null, new PlaygroundContext.Logger() { + public void log(String message) { + } + }); + PlaygroundRunner runner = new PlaygroundRunner(); + return runner.run(script, context); + } + + private static String summarizeMessages(PlaygroundRunner.RunResult result) { + List messages = result.getMessages(); + if (messages.isEmpty()) { + return ""; + } + StringBuilder out = new StringBuilder(); + for (int i = 0; i < messages.size(); i++) { + if (i > 0) { + out.append(" | "); + } + out.append(messages.get(i).text); + } + return out.toString(); + } +} diff --git a/scripts/cn1playground/tools/generate-cn1-access-registry.sh b/scripts/cn1playground/tools/generate-cn1-access-registry.sh index f4a6a2d38d..2a1e393941 100755 --- a/scripts/cn1playground/tools/generate-cn1-access-registry.sh +++ b/scripts/cn1playground/tools/generate-cn1-access-registry.sh @@ -62,9 +62,25 @@ else fi mkdir -p "$BUILD_DIR" -javac -d "$BUILD_DIR" "$SRC" +TOOLS_JAR="${JAVA_HOME:-}/lib/tools.jar" +EXTRA_CP="" +if [ -f "$TOOLS_JAR" ]; then + EXTRA_CP="$TOOLS_JAR" +fi + +if [ -n "$EXTRA_CP" ]; then + javac -cp "$EXTRA_CP" -d "$BUILD_DIR" "$SRC" +else + javac -d "$BUILD_DIR" "$SRC" +fi + +RUNTIME_CP="$BUILD_DIR" +if [ -n "$EXTRA_CP" ]; then + RUNTIME_CP="$BUILD_DIR:$EXTRA_CP" +fi + if [ -n "$CN1_SOURCE_ROOTS_VALUE" ]; then - CN1_SOURCE_ROOTS="$CN1_SOURCE_ROOTS_VALUE" java -cp "$BUILD_DIR" com.codenameone.playground.tools.GenerateCN1AccessRegistry "$OUT" + CN1_SOURCE_ROOTS="$CN1_SOURCE_ROOTS_VALUE" java -cp "$RUNTIME_CP" com.codenameone.playground.tools.GenerateCN1AccessRegistry "$OUT" else - java -cp "$BUILD_DIR" com.codenameone.playground.tools.GenerateCN1AccessRegistry "$OUT" + java -cp "$RUNTIME_CP" com.codenameone.playground.tools.GenerateCN1AccessRegistry "$OUT" fi diff --git a/scripts/cn1playground/tools/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index 2e25794b41..32a67b5b84 100644 --- a/scripts/cn1playground/tools/run-playground-smoke-tests.sh +++ b/scripts/cn1playground/tools/run-playground-smoke-tests.sh @@ -8,14 +8,14 @@ echo "Regenerating CN1 access registry from release sources..." CN1_ACCESS_USE_LOCAL_SOURCES=false bash "$ROOT/tools/generate-cn1-access-registry.sh" echo "Verifying Component is present in generated registry..." -if ! rg -q 'index.put\("com\.codename1\.ui\.Component"' "$ROOT/common/src/main/java/bsh/cn1/GeneratedCN1Access.java"; then +if ! grep -q 'index.put("com.codename1.ui.Component"' "$ROOT/common/src/main/java/bsh/cn1/GeneratedCN1Access.java"; then echo "GeneratedCN1Access is missing com.codename1.ui.Component" >&2 exit 1 fi echo "Verifying key com.codename1.ui classes are present in generated registry..." for cls in Button Container Dialog Display Form Label List TextField BrowserComponent; do - if ! rg -q "index.put\\(\"com\\.codename1\\.ui\\.${cls}\"" "$ROOT/common/src/main/java/bsh/cn1/GeneratedCN1Access.java"; then + if ! grep -q "index.put(\"com.codename1.ui.${cls}\"" "$ROOT/common/src/main/java/bsh/cn1/GeneratedCN1Access.java"; then echo "GeneratedCN1Access is missing com.codename1.ui.${cls}" >&2 exit 1 fi @@ -23,12 +23,16 @@ done echo "Verifying package-private/internal sentinel classes are NOT generated..." for cls in com.codename1.ui.Accessor com.codename1.io.IOAccessor; do - if rg -q "index.put\\(\"${cls}\"" "$ROOT/common/src/main/java/bsh/cn1/GeneratedCN1Access.java"; then + if grep -q "index.put(\"${cls}\"" "$ROOT/common/src/main/java/bsh/cn1/GeneratedCN1Access.java"; then echo "GeneratedCN1Access unexpectedly includes internal class ${cls}" >&2 exit 1 fi done -mvn -pl common -DskipTests test-compile org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ +mvn -pl common -am -DskipTests install +mvn -f common/pom.xml -DskipTests org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ -Dexec.classpathScope=test \ -Dexec.mainClass=com.codenameone.playground.PlaygroundSmokeHarness +mvn -f common/pom.xml -DskipTests org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ + -Dexec.classpathScope=test \ + -Dexec.mainClass=com.codenameone.playground.PlaygroundSyntaxMatrixHarness