From 24cb18d751e8043cbfdce72f91349f365d2d9f95 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:20:57 +0300 Subject: [PATCH 01/23] Add CI workflow for cn1playground language smoke tests --- .github/workflows/cn1playground-language.yml | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/cn1playground-language.yml diff --git a/.github/workflows/cn1playground-language.yml b/.github/workflows/cn1playground-language.yml new file mode 100644 index 0000000000..bf6c59addf --- /dev/null +++ b/.github/workflows/cn1playground-language.yml @@ -0,0 +1,39 @@ +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 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '8' + cache: maven + + - name: Run playground language smoke tests + run: | + set -euo pipefail + cd scripts/cn1playground + tools/run-playground-smoke-tests.sh From da4aed416f6397668c485252d652ff0c58ab53bb Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:27:13 +0300 Subject: [PATCH 02/23] Fix playground workflow script invocation permissions --- .github/workflows/cn1playground-language.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cn1playground-language.yml b/.github/workflows/cn1playground-language.yml index bf6c59addf..e940c8fbc7 100644 --- a/.github/workflows/cn1playground-language.yml +++ b/.github/workflows/cn1playground-language.yml @@ -36,4 +36,4 @@ jobs: run: | set -euo pipefail cd scripts/cn1playground - tools/run-playground-smoke-tests.sh + bash tools/run-playground-smoke-tests.sh From 3cd3e096b6247b0976fe9b28a5528e60588c5b2a Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:40:13 +0300 Subject: [PATCH 03/23] Fix registry generator classpath for JDK8 tools API --- .../tools/generate-cn1-access-registry.sh | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 From b29fbaea90cd3b6fb9b425aa1bf41ea72d231084 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:54:20 +0300 Subject: [PATCH 04/23] Use Java 17 and portable checks in playground smoke CI --- .github/workflows/cn1playground-language.yml | 4 ++-- scripts/cn1playground/tools/run-playground-smoke-tests.sh | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cn1playground-language.yml b/.github/workflows/cn1playground-language.yml index e940c8fbc7..73dc6828f3 100644 --- a/.github/workflows/cn1playground-language.yml +++ b/.github/workflows/cn1playground-language.yml @@ -25,11 +25,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Java 8 + - name: Set up Java 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '8' + java-version: '17' cache: maven - name: Run playground language smoke tests diff --git a/scripts/cn1playground/tools/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index 2e25794b41..1bc912a5b1 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,7 +23,7 @@ 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 From 73abb3b08cf2037e89e521248014b4338f28c5ac Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:02:12 +0300 Subject: [PATCH 05/23] Build dependent modules in playground smoke Maven step --- scripts/cn1playground/tools/run-playground-smoke-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cn1playground/tools/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index 1bc912a5b1..e4d1bfaf51 100644 --- a/scripts/cn1playground/tools/run-playground-smoke-tests.sh +++ b/scripts/cn1playground/tools/run-playground-smoke-tests.sh @@ -29,6 +29,6 @@ for cls in com.codename1.ui.Accessor com.codename1.io.IOAccessor; do fi done -mvn -pl common -DskipTests test-compile org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ +mvn -pl common -am -DskipTests test-compile org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ -Dexec.classpathScope=test \ -Dexec.mainClass=com.codenameone.playground.PlaygroundSmokeHarness From e91778b3e51fae06dcf8c355196369f9d4ec6abb Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:08:27 +0300 Subject: [PATCH 06/23] Run playground smoke harness from common module classpath --- scripts/cn1playground/tools/run-playground-smoke-tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/cn1playground/tools/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index e4d1bfaf51..3b60ebbc0d 100644 --- a/scripts/cn1playground/tools/run-playground-smoke-tests.sh +++ b/scripts/cn1playground/tools/run-playground-smoke-tests.sh @@ -29,6 +29,7 @@ for cls in com.codename1.ui.Accessor com.codename1.io.IOAccessor; do fi done -mvn -pl common -am -DskipTests test-compile org.codehaus.mojo:exec-maven-plugin:3.0.0:java \ +mvn -pl common -am -DskipTests test-compile +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 From 1f9482d14d22144b693b95a547d5b98ab9951d7e Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:28:11 +0300 Subject: [PATCH 07/23] Run playground smoke tests under Xvfb in CI --- .github/workflows/cn1playground-language.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cn1playground-language.yml b/.github/workflows/cn1playground-language.yml index 73dc6828f3..de946be799 100644 --- a/.github/workflows/cn1playground-language.yml +++ b/.github/workflows/cn1playground-language.yml @@ -32,8 +32,16 @@ jobs: 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 - bash tools/run-playground-smoke-tests.sh + xvfb-run -a bash tools/run-playground-smoke-tests.sh From e1285ac4dc8639279a03837aecd1947525b67f57 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:01:05 +0300 Subject: [PATCH 08/23] Install reactor artifacts before running playground harness --- scripts/cn1playground/tools/run-playground-smoke-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cn1playground/tools/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index 3b60ebbc0d..86cf61ba76 100644 --- a/scripts/cn1playground/tools/run-playground-smoke-tests.sh +++ b/scripts/cn1playground/tools/run-playground-smoke-tests.sh @@ -29,7 +29,7 @@ for cls in com.codename1.ui.Accessor com.codename1.io.IOAccessor; do fi done -mvn -pl common -am -DskipTests test-compile +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 From 792e546ff7ea7cb65cd3dfca21c2e6e3b9ecc0f5 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:40:23 +0300 Subject: [PATCH 09/23] Force smoke harness process exit after success --- .../com/codenameone/playground/PlaygroundSmokeHarness.java | 3 +++ 1 file changed, 3 insertions(+) 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 { From a6903ea8a2bc19a58a30a356cf9e1230af5ae038 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:01:20 +0300 Subject: [PATCH 10/23] Add playground syntax matrix harness to smoke test pipeline --- .../PlaygroundSyntaxMatrixHarness.java | 133 ++++++++++++++++++ .../tools/run-playground-smoke-tests.sh | 3 + 2 files changed, 136 insertions(+) create mode 100644 scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java 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..2ffef33d5d --- /dev/null +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -0,0 +1,133 @@ +package com.codenameone.playground; + +import com.codename1.ui.Component; +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; + +/** + * Syntax regression matrix for Playground language support. + * + *

When adding new syntax support, update the expected outcome for the + * relevant test case from FAILURE to SUCCESS.

+ */ +public final class PlaygroundSyntaxMatrixHarness { + private PlaygroundSyntaxMatrixHarness() { + } + + private enum ExpectedOutcome { + SUCCESS, + FAILURE + } + + private static final class Case { + final String name; + final ExpectedOutcome expected; + final String script; + + Case(String name, ExpectedOutcome expected, String script) { + this.name = name; + this.expected = expected; + this.script = script; + } + } + + public static void main(String[] args) { + List cases = new ArrayList(); + // Control cases that should stay green. + cases.add(new Case("lambda_listener", ExpectedOutcome.SUCCESS, + "import com.codename1.ui.*;\n" + + "import com.codename1.ui.layouts.*;\n" + + "Container root = new Container(BoxLayout.y());\n" + + "Button b = new Button(\"Go\");\n" + + "b.addActionListener(e -> {});\n" + + "root.add(b);\n" + + "root;\n")); + cases.add(new Case("anonymous_listener", ExpectedOutcome.SUCCESS, + "import com.codename1.ui.*;\n" + + "import com.codename1.ui.events.*;\n" + + "import com.codename1.ui.layouts.*;\n" + + "Container root = new Container(BoxLayout.y());\n" + + "Button b = new Button(\"Go\");\n" + + "b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) {} });\n" + + "root.add(b);\n" + + "root;\n")); + + // Known syntax gaps: flip these to SUCCESS as support lands. + cases.add(new Case("method_reference", ExpectedOutcome.FAILURE, + "import com.codename1.ui.*;\n" + + "import com.codename1.ui.layouts.*;\n" + + "Container root = new Container(BoxLayout.y());\n" + + "Button b = new Button(\"Go\");\n" + + "b.addActionListener(System.out::println);\n" + + "root.add(b);\n" + + "root;\n")); + cases.add(new Case("try_with_resources_multi", ExpectedOutcome.FAILURE, + "import com.codename1.ui.*;\n" + + "import com.codename1.ui.layouts.*;\n" + + "import java.io.*;\n" + + "Container root = new Container(BoxLayout.y());\n" + + "try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1}); ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n" + + " out.write(in.read());\n" + + "}\n" + + "root;\n")); + cases.add(new Case("enhanced_for_array", ExpectedOutcome.FAILURE, + "import com.codename1.ui.*;\n" + + "import com.codename1.ui.layouts.*;\n" + + "Container root = new Container(BoxLayout.y());\n" + + "int sum = 0;\n" + + "for (int v : new int[]{1,2,3}) {\n" + + " sum += v;\n" + + "}\n" + + "root.add(new Label(\"sum=\" + sum));\n" + + "root;\n")); + + int passed = 0; + for (Case testCase : cases) { + PlaygroundRunner.RunResult result = runSnippet(testCase.script); + boolean success = result.getComponent() instanceof Component; + boolean casePassed = testCase.expected == ExpectedOutcome.SUCCESS ? success : !success; + if (!casePassed) { + throw new IllegalStateException("Case failed: " + testCase.name + + " expected=" + testCase.expected + " messages=" + summarizeMessages(result)); + } + passed++; + } + + System.out.println("Playground syntax matrix passed (" + passed + "/" + cases.size() + ")."); + System.exit(0); + } + + 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/run-playground-smoke-tests.sh b/scripts/cn1playground/tools/run-playground-smoke-tests.sh index 86cf61ba76..32a67b5b84 100644 --- a/scripts/cn1playground/tools/run-playground-smoke-tests.sh +++ b/scripts/cn1playground/tools/run-playground-smoke-tests.sh @@ -33,3 +33,6 @@ 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 From e08c2eb36f347155b54276fb8e01bc79203d2c7b Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:01:25 +0300 Subject: [PATCH 11/23] Use Java text blocks in syntax matrix snippets --- .../PlaygroundSyntaxMatrixHarness.java | 88 +++++++++++-------- 1 file changed, 49 insertions(+), 39 deletions(-) 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 index 2ffef33d5d..b0e771ee2a 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -39,51 +39,61 @@ public static void main(String[] args) { List cases = new ArrayList(); // Control cases that should stay green. cases.add(new Case("lambda_listener", ExpectedOutcome.SUCCESS, - "import com.codename1.ui.*;\n" - + "import com.codename1.ui.layouts.*;\n" - + "Container root = new Container(BoxLayout.y());\n" - + "Button b = new Button(\"Go\");\n" - + "b.addActionListener(e -> {});\n" - + "root.add(b);\n" - + "root;\n")); + """ + 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; + """)); cases.add(new Case("anonymous_listener", ExpectedOutcome.SUCCESS, - "import com.codename1.ui.*;\n" - + "import com.codename1.ui.events.*;\n" - + "import com.codename1.ui.layouts.*;\n" - + "Container root = new Container(BoxLayout.y());\n" - + "Button b = new Button(\"Go\");\n" - + "b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) {} });\n" - + "root.add(b);\n" - + "root;\n")); + """ + 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; + """)); // Known syntax gaps: flip these to SUCCESS as support lands. cases.add(new Case("method_reference", ExpectedOutcome.FAILURE, - "import com.codename1.ui.*;\n" - + "import com.codename1.ui.layouts.*;\n" - + "Container root = new Container(BoxLayout.y());\n" - + "Button b = new Button(\"Go\");\n" - + "b.addActionListener(System.out::println);\n" - + "root.add(b);\n" - + "root;\n")); + """ + 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; + """)); cases.add(new Case("try_with_resources_multi", ExpectedOutcome.FAILURE, - "import com.codename1.ui.*;\n" - + "import com.codename1.ui.layouts.*;\n" - + "import java.io.*;\n" - + "Container root = new Container(BoxLayout.y());\n" - + "try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1}); ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n" - + " out.write(in.read());\n" - + "}\n" - + "root;\n")); + """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + import java.io.*; + Container root = new Container(BoxLayout.y()); + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1}); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + out.write(in.read()); + } + root; + """)); cases.add(new Case("enhanced_for_array", ExpectedOutcome.FAILURE, - "import com.codename1.ui.*;\n" - + "import com.codename1.ui.layouts.*;\n" - + "Container root = new Container(BoxLayout.y());\n" - + "int sum = 0;\n" - + "for (int v : new int[]{1,2,3}) {\n" - + " sum += v;\n" - + "}\n" - + "root.add(new Label(\"sum=\" + sum));\n" - + "root;\n")); + """ + 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; + """)); int passed = 0; for (Case testCase : cases) { From d2b1cc727487358e4405b73a0331f860ba841b83 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:55:37 +0300 Subject: [PATCH 12/23] Stabilize syntax matrix outcomes and always exit process --- .../PlaygroundSyntaxMatrixHarness.java | 145 +++++++++--------- 1 file changed, 76 insertions(+), 69 deletions(-) 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 index b0e771ee2a..1beda3da2d 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -36,79 +36,86 @@ private static final class Case { } public static void main(String[] args) { - List cases = new ArrayList(); - // Control cases that should stay green. - cases.add(new Case("lambda_listener", ExpectedOutcome.SUCCESS, - """ - 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; - """)); - cases.add(new Case("anonymous_listener", ExpectedOutcome.SUCCESS, - """ - 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; - """)); + int exitCode = 0; + try { + List cases = new ArrayList(); + // Control cases that should stay green. + cases.add(new Case("lambda_listener", ExpectedOutcome.SUCCESS, + """ + 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; + """)); + cases.add(new Case("anonymous_listener", ExpectedOutcome.SUCCESS, + """ + 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; + """)); + cases.add(new Case("enhanced_for_array", ExpectedOutcome.SUCCESS, + """ + 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; + """)); - // Known syntax gaps: flip these to SUCCESS as support lands. - cases.add(new Case("method_reference", ExpectedOutcome.FAILURE, - """ - 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; - """)); - cases.add(new Case("try_with_resources_multi", ExpectedOutcome.FAILURE, - """ - import com.codename1.ui.*; - import com.codename1.ui.layouts.*; - import java.io.*; - Container root = new Container(BoxLayout.y()); - try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1}); ByteArrayOutputStream out = new ByteArrayOutputStream()) { - out.write(in.read()); - } - root; - """)); - cases.add(new Case("enhanced_for_array", ExpectedOutcome.FAILURE, - """ - 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; - """)); + // Known syntax gaps: flip these to SUCCESS as support lands. + cases.add(new Case("method_reference", ExpectedOutcome.FAILURE, + """ + 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; + """)); + cases.add(new Case("try_with_resources_multi", ExpectedOutcome.FAILURE, + """ + import com.codename1.ui.*; + import com.codename1.ui.layouts.*; + import java.io.*; + Container root = new Container(BoxLayout.y()); + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1}); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + out.write(in.read()); + } + root; + """)); - int passed = 0; - for (Case testCase : cases) { - PlaygroundRunner.RunResult result = runSnippet(testCase.script); - boolean success = result.getComponent() instanceof Component; - boolean casePassed = testCase.expected == ExpectedOutcome.SUCCESS ? success : !success; - if (!casePassed) { - throw new IllegalStateException("Case failed: " + testCase.name - + " expected=" + testCase.expected + " messages=" + summarizeMessages(result)); + int passed = 0; + for (Case testCase : cases) { + PlaygroundRunner.RunResult result = runSnippet(testCase.script); + boolean success = result.getComponent() instanceof Component; + boolean casePassed = testCase.expected == ExpectedOutcome.SUCCESS ? success : !success; + if (!casePassed) { + throw new IllegalStateException("Case failed: " + testCase.name + + " expected=" + testCase.expected + " messages=" + summarizeMessages(result)); + } + passed++; } - passed++; - } - System.out.println("Playground syntax matrix passed (" + passed + "/" + cases.size() + ")."); - System.exit(0); + 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 PlaygroundRunner.RunResult runSnippet(String script) { From 6c21e10827d55a36d4fc4b0dfe28fb053e06926b Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:27:12 +0300 Subject: [PATCH 13/23] Document playground smoke workflow and syntax rollout process --- scripts/cn1playground/README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) 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. From c7cd472ec3ed243cf667dd01f984482ef9fa7106 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:34:28 +0300 Subject: [PATCH 14/23] Expand syntax matrix to table-driven parse/eval coverage --- .../PlaygroundSyntaxMatrixHarness.java | 228 +++++++++++++++--- 1 file changed, 193 insertions(+), 35 deletions(-) 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 index 1beda3da2d..108e8aa995 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -1,6 +1,5 @@ package com.codenameone.playground; -import com.codename1.ui.Component; import com.codename1.ui.Container; import com.codename1.ui.Display; import com.codename1.ui.Form; @@ -9,10 +8,7 @@ import java.util.List; /** - * Syntax regression matrix for Playground language support. - * - *

When adding new syntax support, update the expected outcome for the - * relevant test case from FAILURE to SUCCESS.

+ * Table-driven syntax regression matrix for playground language support. */ public final class PlaygroundSyntaxMatrixHarness { private PlaygroundSyntaxMatrixHarness() { @@ -20,18 +16,21 @@ private PlaygroundSyntaxMatrixHarness() { private enum ExpectedOutcome { SUCCESS, - FAILURE + PARSE_ERROR, + EVAL_ERROR } private static final class Case { final String name; - final ExpectedOutcome expected; - final String script; + final String sourceSnippet; + final ExpectedOutcome expectedOutcome; + final String expectedDiagnosticSubstring; - Case(String name, ExpectedOutcome expected, String script) { + Case(String name, String sourceSnippet, ExpectedOutcome expectedOutcome, String expectedDiagnosticSubstring) { this.name = name; - this.expected = expected; - this.script = script; + this.sourceSnippet = sourceSnippet; + this.expectedOutcome = expectedOutcome; + this.expectedDiagnosticSubstring = expectedDiagnosticSubstring; } } @@ -39,9 +38,9 @@ public static void main(String[] args) { int exitCode = 0; try { List cases = new ArrayList(); - // Control cases that should stay green. - cases.add(new Case("lambda_listener", ExpectedOutcome.SUCCESS, - """ + + // 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()); @@ -49,9 +48,8 @@ public static void main(String[] args) { b.addActionListener(e -> {}); root.add(b); root; - """)); - cases.add(new Case("anonymous_listener", ExpectedOutcome.SUCCESS, - """ + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("control_anonymous_listener", """ import com.codename1.ui.*; import com.codename1.ui.events.*; import com.codename1.ui.layouts.*; @@ -60,23 +58,21 @@ public static void main(String[] args) { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) {} }); root.add(b); root; - """)); - cases.add(new Case("enhanced_for_array", ExpectedOutcome.SUCCESS, - """ + """, 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 v : new int[]{1,2,3}) { - sum += v; + for (int i = 0; i < 3; i++) { + sum += i; } root.add(new Label("sum=" + sum)); root; - """)); + """, ExpectedOutcome.SUCCESS, null)); - // Known syntax gaps: flip these to SUCCESS as support lands. - cases.add(new Case("method_reference", ExpectedOutcome.FAILURE, - """ + // Method references. + cases.add(new Case("method_reference_type", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; Container root = new Container(BoxLayout.y()); @@ -84,9 +80,39 @@ public static void main(String[] args) { b.addActionListener(System.out::println); root.add(b); root; - """)); - cases.add(new Case("try_with_resources_multi", ExpectedOutcome.FAILURE, - """ + """, 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.*; + import java.io.*; + Container root = new Container(BoxLayout.y()); + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1,2,3})) { + root.add(new Label("ok=" + in.read())); + } + root; + """, ExpectedOutcome.SUCCESS, null)); + cases.add(new Case("twr_multiple_resources", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; import java.io.*; @@ -95,16 +121,125 @@ public static void main(String[] args) { out.write(in.read()); } root; - """)); + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + 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 (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1});) { + 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 (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1})) { + 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.script); - boolean success = result.getComponent() instanceof Component; - boolean casePassed = testCase.expected == ExpectedOutcome.SUCCESS ? success : !success; - if (!casePassed) { + PlaygroundRunner.RunResult result = runSnippet(testCase.sourceSnippet); + ExpectedOutcome actual = classify(result); + if (actual != testCase.expectedOutcome) { throw new IllegalStateException("Case failed: " + testCase.name - + " expected=" + testCase.expected + " messages=" + summarizeMessages(result)); + + " 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++; } @@ -118,6 +253,29 @@ public static void main(String[] args) { } } + 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()); From aecea500ad6e06a857dc36de6c3088c22ba26b17 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:28:31 +0300 Subject: [PATCH 15/23] Mark try-with-resources matrix cases as current parse gaps --- .../codenameone/playground/PlaygroundSyntaxMatrixHarness.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 108e8aa995..3bb884a40f 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -111,7 +111,7 @@ public static void main(String[] args) { root.add(new Label("ok=" + in.read())); } root; - """, ExpectedOutcome.SUCCESS, null)); + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); cases.add(new Case("twr_multiple_resources", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; @@ -147,7 +147,7 @@ public static void main(String[] args) { } } root; - """, ExpectedOutcome.SUCCESS, null)); + """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); // Enhanced-for arrays. cases.add(new Case("enhanced_for_primitive_array", """ From a9281ef1d4c9114f1cf85d500e2ecdba941f063d Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:49:30 +0300 Subject: [PATCH 16/23] Enable try-with-resources without catch/finally in parser --- scripts/cn1playground/common/src/main/java/bsh/Parser.java | 1 + .../playground/PlaygroundSyntaxMatrixHarness.java | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) 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/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java index 3bb884a40f..c40c71c826 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -111,7 +111,7 @@ public static void main(String[] args) { root.add(new Label("ok=" + in.read())); } root; - """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + """, ExpectedOutcome.SUCCESS, null)); cases.add(new Case("twr_multiple_resources", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; @@ -121,7 +121,7 @@ public static void main(String[] args) { out.write(in.read()); } root; - """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + """, ExpectedOutcome.SUCCESS, null)); cases.add(new Case("twr_trailing_semicolon", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; @@ -147,7 +147,7 @@ public static void main(String[] args) { } } root; - """, ExpectedOutcome.PARSE_ERROR, "Parse error:")); + """, ExpectedOutcome.SUCCESS, null)); // Enhanced-for arrays. cases.add(new Case("enhanced_for_primitive_array", """ From 8af87c8e4d94ce41d738a02b4b8cb4bd841f9764 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:49:55 +0300 Subject: [PATCH 17/23] Throw evaluation error for enhanced-for over null --- .../common/src/main/java/bsh/BSHEnhancedForStatement.java | 3 +++ 1 file changed, 3 insertions(+) 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 { From a72f2af63a450d67f8d71db4caa412f0f35f43c8 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:05:18 +0300 Subject: [PATCH 18/23] Use local AutoCloseable resources in TWR matrix cases --- .../PlaygroundSyntaxMatrixHarness.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) 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 index c40c71c826..dcc0f176c0 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -105,29 +105,35 @@ public static void main(String[] args) { cases.add(new Case("twr_single_resource", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - import java.io.*; + class Res implements AutoCloseable { + public void close() {} + } Container root = new Container(BoxLayout.y()); - try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1,2,3})) { - root.add(new Label("ok=" + in.read())); + 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.*; + class Res implements AutoCloseable { + public void close() {} + } Container root = new Container(BoxLayout.y()); - try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1}); ByteArrayOutputStream out = new ByteArrayOutputStream()) { - out.write(in.read()); + try (Res in = new Res(); Res out = new Res()) { + 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.*; + class Res implements AutoCloseable { + public void close() {} + } Container root = new Container(BoxLayout.y()); - try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1});) { + try (Res in = new Res();) { root.add(new Label("ok")); } root; @@ -135,9 +141,11 @@ public static void main(String[] args) { cases.add(new Case("twr_nested_try_catch_finally", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - import java.io.*; + class Res implements AutoCloseable { + public void close() {} + } Container root = new Container(BoxLayout.y()); - try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1})) { + try (Res in = new Res()) { try { root.add(new Label("inner")); } catch (RuntimeException ex) { From c60e28b0dd9907c818b698a87fb6c8031f8239ed Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:51:29 +0300 Subject: [PATCH 19/23] Use StringReader resources in TWR matrix cases --- .../PlaygroundSyntaxMatrixHarness.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) 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 index dcc0f176c0..3618a6253c 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -105,11 +105,9 @@ public static void main(String[] args) { cases.add(new Case("twr_single_resource", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - class Res implements AutoCloseable { - public void close() {} - } + import java.io.*; Container root = new Container(BoxLayout.y()); - try (Res in = new Res()) { + try (StringReader in = new StringReader("abc")) { root.add(new Label("ok")); } root; @@ -117,11 +115,9 @@ public void close() {} cases.add(new Case("twr_multiple_resources", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - class Res implements AutoCloseable { - public void close() {} - } + import java.io.*; Container root = new Container(BoxLayout.y()); - try (Res in = new Res(); Res out = new Res()) { + try (StringReader in = new StringReader("a"); StringReader out = new StringReader("b")) { root.add(new Label("ok")); } root; @@ -129,11 +125,9 @@ public void close() {} cases.add(new Case("twr_trailing_semicolon", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - class Res implements AutoCloseable { - public void close() {} - } + import java.io.*; Container root = new Container(BoxLayout.y()); - try (Res in = new Res();) { + try (StringReader in = new StringReader("a");) { root.add(new Label("ok")); } root; @@ -141,11 +135,9 @@ public void close() {} cases.add(new Case("twr_nested_try_catch_finally", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - class Res implements AutoCloseable { - public void close() {} - } + import java.io.*; Container root = new Container(BoxLayout.y()); - try (Res in = new Res()) { + try (StringReader in = new StringReader("abc")) { try { root.add(new Label("inner")); } catch (RuntimeException ex) { From a9f1b8855d926cb2fbe54a4d0b5e609eb79ca862 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:54:58 +0300 Subject: [PATCH 20/23] Enable BeanShell class generation for inline class declarations --- .../src/main/java/bsh/ClassGenerator.java | 302 ++++- .../src/main/java/bsh/ClassGeneratorUtil.java | 1000 ++++++++++++++++- .../PlaygroundSyntaxMatrixHarness.java | 6 +- 3 files changed, 1272 insertions(+), 36 deletions(-) diff --git a/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java b/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java index d1f4a1488f..4f501078af 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java +++ b/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java @@ -15,54 +15,296 @@ * 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.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + public final class ClassGenerator { - public enum Type { - CLASS, INTERFACE, ENUM + + enum Type { CLASS, INTERFACE, ENUM } + + private static ClassGenerator cg; + + public static ClassGenerator getClassGenerator() { + if (cg == null) { + cg = new ClassGenerator(); + } + + 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); } - 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(); + /** + * 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); - private ClassNodeFilter() { + // 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()) + saveClasses(className, code); + + // 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('$', '.')); - public boolean isVisible(Node node) { - return false; + // 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 void saveClasses(String className, byte[] code) { + String dir = Interpreter.getSaveClassesDir(); + if (dir != null) try + ( FileOutputStream out = new FileOutputStream(dir + "/" + className + ".class") ) { + out.write(code); + } catch (IOException e) { + e.printStackTrace(); } } - 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/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java index 3618a6253c..fb23af2349 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java @@ -105,9 +105,11 @@ public static void main(String[] args) { cases.add(new Case("twr_single_resource", """ import com.codename1.ui.*; import com.codename1.ui.layouts.*; - import java.io.*; + class Res implements AutoCloseable { + public void close() {} + } Container root = new Container(BoxLayout.y()); - try (StringReader in = new StringReader("abc")) { + try (Res in = new Res()) { root.add(new Label("ok")); } root; From 350450eb26d7cb1055fdd5f23c108839317487f5 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:33:05 +0300 Subject: [PATCH 21/23] Handle superclass invocation checked exceptions in Name --- .../cn1playground/common/src/main/java/bsh/Name.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/cn1playground/common/src/main/java/bsh/Name.java b/scripts/cn1playground/common/src/main/java/bsh/Name.java index 205d945069..f6c33f57f2 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/Name.java +++ b/scripts/cn1playground/common/src/main/java/bsh/Name.java @@ -25,6 +25,7 @@ *****************************************************************************/ package bsh; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -808,9 +809,14 @@ public Object invokeMethod( if ( classNameSpace != null ) { Object instance = classNameSpace.getClassInstance(); Class classStatic = classNameSpace.classStatic; - - return ClassGenerator.getClassGenerator() - .invokeSuperclassMethod( bcm, instance, classStatic, methodName, args ); + try { + return ClassGenerator.getClassGenerator() + .invokeSuperclassMethod( bcm, instance, classStatic, methodName, args ); + } catch (InvocationTargetException e) { + throw new UtilTargetError(e.getTargetException()); + } catch (ReflectError e) { + throw new UtilEvalError("Error invoking superclass method: " + e.getMessage()); + } } } From 126d6cfc5267f3f5b1473b61f67a18db174843b4 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:00:34 +0300 Subject: [PATCH 22/23] Support inline AutoCloseable class snippets without ASM generator --- .../src/main/java/bsh/ClassGenerator.java | 302 +---- .../src/main/java/bsh/ClassGeneratorUtil.java | 1000 +---------------- .../common/src/main/java/bsh/Name.java | 12 +- .../playground/PlaygroundRunner.java | 40 + 4 files changed, 77 insertions(+), 1277 deletions(-) diff --git a/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java b/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java index 4f501078af..d1f4a1488f 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java +++ b/scripts/cn1playground/common/src/main/java/bsh/ClassGenerator.java @@ -15,296 +15,54 @@ * 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.This.Keys.BSHSUPER; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; +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. + */ public final class ClassGenerator { - - enum Type { CLASS, INTERFACE, ENUM } - - private static ClassGenerator cg; - - public static ClassGenerator getClassGenerator() { - if (cg == null) { - cg = new ClassGenerator(); - } - - 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); + public enum Type { + CLASS, INTERFACE, ENUM } - /** - * 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); + 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(); - // 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()) - saveClasses(className, code); - - // Define the new class in the classloader - genClass = bcm.defineClass(fqClassName, code); - Interpreter.debug("Define ", fqClassName, " as ", genClass); + private ClassNodeFilter() { } - // 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 void saveClasses(String className, byte[] code) { - String dir = Interpreter.getSaveClassesDir(); - if (dir != null) try - ( FileOutputStream out = new FileOutputStream(dir + "/" + className + ".class") ) { - out.write(code); - } catch (IOException e) { - e.printStackTrace(); + public boolean isVisible(Node node) { + return false; } } - 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 static final ClassGenerator INSTANCE = new ClassGenerator(); - return vars.toArray(new Variable[vars.size()]); + private ClassGenerator() { } - 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 static ClassGenerator getClassGenerator() { + return INSTANCE; } - - /** - * 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 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); } - /** 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); + 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); } - } diff --git a/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java b/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java index ca4d8871b0..8483355285 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java +++ b/scripts/cn1playground/common/src/main/java/bsh/ClassGeneratorUtil.java @@ -1,1003 +1,11 @@ -/***************************************************************************** - * 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; - /** - * 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 + * Placeholder for the disabled class-generation pipeline. */ -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); +public final class ClassGeneratorUtil { + public static final int DEFAULTCONSTRUCTOR = -1; - // 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); - } + private ClassGeneratorUtil() { } } diff --git a/scripts/cn1playground/common/src/main/java/bsh/Name.java b/scripts/cn1playground/common/src/main/java/bsh/Name.java index f6c33f57f2..205d945069 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/Name.java +++ b/scripts/cn1playground/common/src/main/java/bsh/Name.java @@ -25,7 +25,6 @@ *****************************************************************************/ package bsh; -import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -809,14 +808,9 @@ public Object invokeMethod( if ( classNameSpace != null ) { Object instance = classNameSpace.getClassInstance(); Class classStatic = classNameSpace.classStatic; - try { - return ClassGenerator.getClassGenerator() - .invokeSuperclassMethod( bcm, instance, classStatic, methodName, args ); - } catch (InvocationTargetException e) { - throw new UtilTargetError(e.getTargetException()); - } catch (ReflectError e) { - throw new UtilEvalError("Error invoking superclass method: " + e.getMessage()); - } + + return ClassGenerator.getClassGenerator() + .invokeSuperclassMethod( bcm, instance, classStatic, methodName, args ); } } 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); From 76c5eca24484477ed531b4b50e85668016fe26a8 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:00:39 +0300 Subject: [PATCH 23/23] Restore BeanShell class generation for playground runtime --- .../src/main/java/bsh/ClassGenerator.java | 293 ++++- .../src/main/java/bsh/ClassGeneratorUtil.java | 1000 ++++++++++++++++- 2 files changed, 1258 insertions(+), 35 deletions(-) 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); + } } }