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);
+ }
}
}