From 3f44e6d8b03be3a18de334ae5d4037ee937a29f1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 14 May 2026 03:54:40 +0300 Subject: [PATCH] Fix JavaDoc snippet validator and the snippets it surfaced The headless snippet validator stopped catching real issues after a chain of refactors: PlaygroundProjectExporter pulled in net.sf.zipme.* breaking the harness build, the resolveDisplayBinding wrappers were reverted out of PlaygroundRunner.bindGlobals so it tripped on ExceptionInInitializerError, and CN1's Base64 grew a Simd dep that forced Display.impl to be live before any encoding could happen. Repairs: - Add ZipSupport cn1lib to the harness classpath - Restore resolveDisplay/UiManagerBinding in PlaygroundRunner - Encode URIs via java.util.Base64 instead of reflecting into CN1's - Best-effort Display.init from the locally-built JavaSE jar so "Undefined argument: icon" surfaces instead of an NPE - Tighten the harness so undefined-identifier eval errors fail, while genuine headless-runtime failures still pass Then fix the snippets the now-working validator flags: - SpanLabel/SpanButton/MultiButton/CheckBox/RadioButton/ButtonGroup/ Label/ImageViewer: add the missing Form/Image setup and hi.show() - Preferences (and io/package-info copy): declare String myToken - MathUtil.compare(float|double): turn the bare compareTo line into a runnable Form demo - Component.getAllStyles / Graphics.fillArc: rewrite the broken "new Painter(cmp) { switch (style) ... }" pseudo-code (Painter is an interface; style/cmp were undefined) as a runnable Painter demo - Validator: wrap the Constraint sample in a runnable Form - system/package-info: relabel the Objective-C example block from java to objc - Add the four genuinely-illustrative GroupLayout/LayoutStyle method- signature snippets to the exclusions list with documented reasons Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/codename1/components/ImageViewer.java | 2 + .../com/codename1/components/MultiButton.java | 6 ++ .../com/codename1/components/SpanButton.java | 5 + .../com/codename1/components/SpanLabel.java | 5 + .../src/com/codename1/io/Preferences.java | 1 + .../src/com/codename1/io/package-info.java | 1 + .../com/codename1/system/package-info.java | 2 +- .../src/com/codename1/ui/ButtonGroup.java | 5 + .../src/com/codename1/ui/CheckBox.java | 5 + .../src/com/codename1/ui/Component.java | 53 +++------- .../src/com/codename1/ui/Graphics.java | 50 +++------ CodenameOne/src/com/codename1/ui/Label.java | 5 + .../src/com/codename1/ui/RadioButton.java | 5 + .../codename1/ui/validation/Validator.java | 5 + .../src/com/codename1/util/MathUtil.java | 14 ++- .../playground/PlaygroundRunner.java | 24 ++++- .../JavaSnippetToPlaygroundUriHarness.java | 100 ++++++++++++++++-- scripts/java-snippet-to-playground-uri.sh | 33 +++++- .../java-snippet-validation-exclusions.jsonl | 4 + 19 files changed, 233 insertions(+), 92 deletions(-) diff --git a/CodenameOne/src/com/codename1/components/ImageViewer.java b/CodenameOne/src/com/codename1/components/ImageViewer.java index da4df70257..d6d9ef8d27 100644 --- a/CodenameOne/src/com/codename1/components/ImageViewer.java +++ b/CodenameOne/src/com/codename1/components/ImageViewer.java @@ -47,8 +47,10 @@ /// /// ```java /// Form hi = new Form("ImageViewer", new BorderLayout()); +/// Image duke = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f); /// ImageViewer iv = new ImageViewer(duke); /// hi.add(BorderLayout.CENTER, iv); +/// hi.show(); /// ``` /// /// You can simulate pinch to zoom on the simulator by dragging the right button away from the top left corner to diff --git a/CodenameOne/src/com/codename1/components/MultiButton.java b/CodenameOne/src/com/codename1/components/MultiButton.java index a7ad638cb6..9c5e35e779 100644 --- a/CodenameOne/src/com/codename1/components/MultiButton.java +++ b/CodenameOne/src/com/codename1/components/MultiButton.java @@ -50,6 +50,11 @@ /// a lead component. Up to 4 rows are supported. /// /// ```java +/// Form hi = new Form("Test MultiButton", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Button", 3.0f); +/// Image emblem = FontImage.createMaterial(FontImage.MATERIAL_ARROW_FORWARD, "Button", 3.0f); +/// /// MultiButton twoLinesNoIcon = new MultiButton("MultiButton"); /// twoLinesNoIcon.setTextLine2("Line 2"); /// MultiButton oneLineIconEmblem = new MultiButton("Icon + Emblem"); @@ -83,6 +88,7 @@ /// add(twoLinesIconEmblemHorizontal). /// add(twoLinesIconCheckBox). /// add(fourLinesIcon); +/// hi.show(); /// ``` /// /// @author Shai Almog diff --git a/CodenameOne/src/com/codename1/components/SpanButton.java b/CodenameOne/src/com/codename1/components/SpanButton.java index deaa2f05f0..be2dbce86c 100644 --- a/CodenameOne/src/com/codename1/components/SpanButton.java +++ b/CodenameOne/src/com/codename1/components/SpanButton.java @@ -49,9 +49,14 @@ /// button has the UIID style of a button. /// /// ```java +/// Form hi = new Form("Test SpanButton", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Button", 3.0f); +/// /// SpanButton sb = new SpanButton("SpanButton is a composite component (lead component) that looks/acts like a Button but can break lines rather than crop them when the text is very long."); /// sb.setIcon(icon); /// hi.add(sb); +/// hi.show(); /// ``` /// /// @author Shai Almog diff --git a/CodenameOne/src/com/codename1/components/SpanLabel.java b/CodenameOne/src/com/codename1/components/SpanLabel.java index 27be8af7e7..9fa25e38ff 100644 --- a/CodenameOne/src/com/codename1/components/SpanLabel.java +++ b/CodenameOne/src/com/codename1/components/SpanLabel.java @@ -44,6 +44,10 @@ /// on a text area combined with a label. /// /// ```java +/// Form hi = new Form("Test SpanLabel", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f); +/// /// SpanLabel d = new SpanLabel("Default SpanLabel that can seamlessly line break when the text is really long."); /// d.setIcon(icon); /// SpanLabel l = new SpanLabel("NORTH Positioned Icon SpanLabel that can seamlessly line break when the text is really long."); @@ -56,6 +60,7 @@ /// c.setIcon(icon); /// c.setIconPosition(BorderLayout.EAST); /// hi.add(d).add(l).add(r).add(c); +/// hi.show(); /// ``` /// /// @author Shai Almog diff --git a/CodenameOne/src/com/codename1/io/Preferences.java b/CodenameOne/src/com/codename1/io/Preferences.java index b64c7a7f8f..937c84479f 100644 --- a/CodenameOne/src/com/codename1/io/Preferences.java +++ b/CodenameOne/src/com/codename1/io/Preferences.java @@ -35,6 +35,7 @@ /// /// ```java /// // save a token to storage +/// String myToken = "abc123"; /// Preferences.set("token", myToken); /// /// // get the token from storage or null if it isn't there diff --git a/CodenameOne/src/com/codename1/io/package-info.java b/CodenameOne/src/com/codename1/io/package-info.java index 99281bc562..bb81bf9584 100644 --- a/CodenameOne/src/com/codename1/io/package-info.java +++ b/CodenameOne/src/com/codename1/io/package-info.java @@ -58,6 +58,7 @@ /// /// ```java /// // save a token to storage +/// String myToken = "abc123"; /// Preferences.set("token", myToken); /// /// // get the token from storage or null if it isn't there diff --git a/CodenameOne/src/com/codename1/system/package-info.java b/CodenameOne/src/com/codename1/system/package-info.java index 66468b03b3..a84cd1bc64 100644 --- a/CodenameOne/src/com/codename1/system/package-info.java +++ b/CodenameOne/src/com/codename1/system/package-info.java @@ -63,7 +63,7 @@ /// combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C /// (by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.: /// -/// ```java +/// ```objc /// @interface com_my_code_MyNative : NSObject { /// } /// - (id)init; diff --git a/CodenameOne/src/com/codename1/ui/ButtonGroup.java b/CodenameOne/src/com/codename1/ui/ButtonGroup.java index ee81bab76f..875de58e88 100644 --- a/CodenameOne/src/com/codename1/ui/ButtonGroup.java +++ b/CodenameOne/src/com/codename1/ui/ButtonGroup.java @@ -36,6 +36,10 @@ /// the specific `ButtonGroup`. /// /// ```java +/// Form hi = new Form("Test ButtonGroup", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "CheckBox", 3.0f); +/// /// CheckBox cb1 = new CheckBox("CheckBox No Icon"); /// cb1.setSelected(true); /// CheckBox cb2 = new CheckBox("CheckBox With Icon", icon); @@ -49,6 +53,7 @@ /// new ButtonGroup(rb1, rb2, rb3); /// rb2.setSelected(true); /// hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3); +/// hi.show(); /// ``` /// /// @author Nir Shabi diff --git a/CodenameOne/src/com/codename1/ui/CheckBox.java b/CodenameOne/src/com/codename1/ui/CheckBox.java index 3a31e4fe3d..1acdeb92c6 100644 --- a/CodenameOne/src/com/codename1/ui/CheckBox.java +++ b/CodenameOne/src/com/codename1/ui/CheckBox.java @@ -38,6 +38,10 @@ /// mode using the `com.codename1.ui.Button#setToggle(boolean)` API. /// /// ```java +/// Form hi = new Form("Test CheckBox", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "CheckBox", 3.0f); +/// /// CheckBox cb1 = new CheckBox("CheckBox No Icon"); /// cb1.setSelected(true); /// CheckBox cb2 = new CheckBox("CheckBox With Icon", icon); @@ -51,6 +55,7 @@ /// new ButtonGroup(rb1, rb2, rb3); /// rb2.setSelected(true); /// hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3); +/// hi.show(); /// ``` /// /// @author Chen Fishbein diff --git a/CodenameOne/src/com/codename1/ui/Component.java b/CodenameOne/src/com/codename1/ui/Component.java index a143ef21ee..368e0ec7e3 100644 --- a/CodenameOne/src/com/codename1/ui/Component.java +++ b/CodenameOne/src/com/codename1/ui/Component.java @@ -692,50 +692,29 @@ public Object getNativeOverlay() { /// style will be meaningless and will return 0 values. Usage: /// /// ```java - /// Painter p = new Painter(cmp) { + /// Form hi = new Form("Painter via getAllStyles", new BorderLayout()); + /// Container cmp = new Container(); + /// cmp.setPreferredSize(new Dimension(300, 300)); + /// Painter p = new Painter() { /// public void paint(Graphics g, Rectangle rect) { /// boolean antiAliased = g.isAntiAliased(); /// g.setAntiAliased(true); - /// int r = Math.min(rect.getWidth(), rect.getHeight())/2; - /// int x = rect.getX() + rect.getWidth()/2 - r; - /// int y = rect.getY() + rect.getHeight()/2 - r; - /// switch (style) { - /// case CircleButtonStrokedDark: - /// case CircleButtonStrokedLight: { - /// if (cmp.getStyle().getBgTransparency() != 0) { - /// int alpha = cmp.getStyle().getBgTransparency(); - /// if (alpha <0) { - /// alpha = 0xff; - /// } - /// g.setColor(cmp.getStyle().getBgColor()); - /// g.setAlpha(alpha); - /// g.fillArc(x, y, 2*r-1, 2*r-1, 0, 360); - /// g.setAlpha(0xff); - /// } - /// g.setColor(cmp.getStyle().getFgColor()); - /// g.drawArc(x, y, 2*r-1, 2*r-1, 0, 360); - /// break; - /// } - /// case CircleButtonFilledDark: - /// case CircleButtonFilledLight: - /// case CircleButtonTransparentDark: - /// case CircleButtonTransparentLight: { - /// int alpha = cmp.getStyle().getBgTransparency(); - /// if (alpha < 0) { - /// alpha = 0xff; - /// } - /// g.setAlpha(alpha); - /// g.setColor(cmp.getStyle().getBgColor()); - /// g.fillArc(x, y, 2*r, 2*r, 0, 360); - /// g.setAlpha(0xff); - /// break; - /// } - /// } - /// + /// int r = Math.min(rect.getWidth(), rect.getHeight()) / 2; + /// int x = rect.getX() + rect.getWidth() / 2 - r; + /// int y = rect.getY() + rect.getHeight() / 2 - r; + /// g.setColor(cmp.getStyle().getBgColor()); + /// g.fillArc(x, y, 2 * r, 2 * r, 0, 360); + /// g.setColor(cmp.getStyle().getFgColor()); + /// g.drawArc(x, y, 2 * r - 1, 2 * r - 1, 0, 360); /// g.setAntiAliased(antiAliased); /// } /// }; + /// cmp.getAllStyles().setBgColor(0x4488ff); + /// cmp.getAllStyles().setBgTransparency(255); + /// cmp.getAllStyles().setFgColor(0xffffff); /// cmp.getAllStyles().setBgPainter(p); + /// hi.add(BorderLayout.CENTER, cmp); + /// hi.show(); /// ``` /// /// #### Returns diff --git a/CodenameOne/src/com/codename1/ui/Graphics.java b/CodenameOne/src/com/codename1/ui/Graphics.java index 4c5f6c65fe..7448e8a019 100644 --- a/CodenameOne/src/com/codename1/ui/Graphics.java +++ b/CodenameOne/src/com/codename1/ui/Graphics.java @@ -644,50 +644,26 @@ public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int /// degrees. Usage: /// /// ```java - /// Painter p = new Painter(cmp) { + /// Form hi = new Form("fillArc / drawArc", new BorderLayout()); + /// Container cmp = new Container(); + /// cmp.setPreferredSize(new Dimension(300, 300)); + /// Painter p = new Painter() { /// public void paint(Graphics g, Rectangle rect) { /// boolean antiAliased = g.isAntiAliased(); /// g.setAntiAliased(true); - /// int r = Math.min(rect.getWidth(), rect.getHeight())/2; - /// int x = rect.getX() + rect.getWidth()/2 - r; - /// int y = rect.getY() + rect.getHeight()/2 - r; - /// switch (style) { - /// case CircleButtonStrokedDark: - /// case CircleButtonStrokedLight: { - /// if (cmp.getStyle().getBgTransparency() != 0) { - /// int alpha = cmp.getStyle().getBgTransparency(); - /// if (alpha <0) { - /// alpha = 0xff; - /// } - /// g.setColor(cmp.getStyle().getBgColor()); - /// g.setAlpha(alpha); - /// g.fillArc(x, y, 2*r-1, 2*r-1, 0, 360); - /// g.setAlpha(0xff); - /// } - /// g.setColor(cmp.getStyle().getFgColor()); - /// g.drawArc(x, y, 2*r-1, 2*r-1, 0, 360); - /// break; - /// } - /// case CircleButtonFilledDark: - /// case CircleButtonFilledLight: - /// case CircleButtonTransparentDark: - /// case CircleButtonTransparentLight: { - /// int alpha = cmp.getStyle().getBgTransparency(); - /// if (alpha < 0) { - /// alpha = 0xff; - /// } - /// g.setAlpha(alpha); - /// g.setColor(cmp.getStyle().getBgColor()); - /// g.fillArc(x, y, 2*r, 2*r, 0, 360); - /// g.setAlpha(0xff); - /// break; - /// } - /// } - /// + /// int r = Math.min(rect.getWidth(), rect.getHeight()) / 2; + /// int x = rect.getX() + rect.getWidth() / 2 - r; + /// int y = rect.getY() + rect.getHeight() / 2 - r; + /// g.setColor(0x4488ff); + /// g.fillArc(x, y, 2 * r, 2 * r, 0, 360); + /// g.setColor(0xffffff); + /// g.drawArc(x, y, 2 * r - 1, 2 * r - 1, 0, 360); /// g.setAntiAliased(antiAliased); /// } /// }; /// cmp.getAllStyles().setBgPainter(p); + /// hi.add(BorderLayout.CENTER, cmp); + /// hi.show(); /// ``` /// /// #### Parameters diff --git a/CodenameOne/src/com/codename1/ui/Label.java b/CodenameOne/src/com/codename1/ui/Label.java index b4a075d0c7..dcc1e8f150 100644 --- a/CodenameOne/src/com/codename1/ui/Label.java +++ b/CodenameOne/src/com/codename1/ui/Label.java @@ -47,6 +47,10 @@ /// Label text can be positioned in one of 4 locations as such: /// /// ```java +/// Form hi = new Form("Test Label", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f); +/// /// Label left = new Label("Left", icon); /// left.setTextPosition(Component.LEFT); /// Label right = new Label("Right", icon); @@ -56,6 +60,7 @@ /// Label top = new Label("Top", icon); /// top.setTextPosition(Component.TOP); /// hi.add(left).add(right).add(bottom).add(top); +/// hi.show(); /// ``` /// /// @author Chen Fishbein diff --git a/CodenameOne/src/com/codename1/ui/RadioButton.java b/CodenameOne/src/com/codename1/ui/RadioButton.java index 8e25146d48..027d4d0d35 100644 --- a/CodenameOne/src/com/codename1/ui/RadioButton.java +++ b/CodenameOne/src/com/codename1/ui/RadioButton.java @@ -38,6 +38,10 @@ /// mode using the `com.codename1.ui.Button#setToggle(boolean)` API. /// /// ```java +/// Form hi = new Form("Test RadioButton", BoxLayout.y()); +/// +/// Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "CheckBox", 3.0f); +/// /// CheckBox cb1 = new CheckBox("CheckBox No Icon"); /// cb1.setSelected(true); /// CheckBox cb2 = new CheckBox("CheckBox With Icon", icon); @@ -51,6 +55,7 @@ /// new ButtonGroup(rb1, rb2, rb3); /// rb2.setSelected(true); /// hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3); +/// hi.show(); /// ``` /// /// @author Chen Fishbein diff --git a/CodenameOne/src/com/codename1/ui/validation/Validator.java b/CodenameOne/src/com/codename1/ui/validation/Validator.java index 960d542ec2..d70f4fb510 100644 --- a/CodenameOne/src/com/codename1/ui/validation/Validator.java +++ b/CodenameOne/src/com/codename1/ui/validation/Validator.java @@ -62,6 +62,9 @@ /// [this discussion](https://stackoverflow.com/questions/48481888/codename-one-regexconstraint-to-check-a-valid-phone-number/48483465#48483465) on StackOverflow): /// /// ```java +/// Form hi = new Form("Phone Validator", BoxLayout.y()); +/// TextField phone = new TextField("", "Phone"); +/// Validator val = new Validator(); /// val.addConstraint(phone, new Constraint() { /// public boolean isValid(Object value) { /// String v = (String)value; @@ -78,6 +81,8 @@ /// return "Must be valid phone number"; /// } /// }); +/// hi.add(phone); +/// hi.show(); /// ``` /// /// @author Shai Almog diff --git a/CodenameOne/src/com/codename1/util/MathUtil.java b/CodenameOne/src/com/codename1/util/MathUtil.java index d7a557a6c7..8063d5ef5f 100644 --- a/CodenameOne/src/com/codename1/util/MathUtil.java +++ b/CodenameOne/src/com/codename1/util/MathUtil.java @@ -1424,7 +1424,12 @@ public static long floor(double a) { /// integer that would be returned by the call: /// /// ```java - /// new Float(f1).compareTo(new Float(f2)) + /// float f1 = 1.5f; + /// float f2 = 2.5f; + /// int result = new Float(f1).compareTo(new Float(f2)); + /// Form hi = new Form("MathUtil.compare(float)", BoxLayout.y()); + /// hi.add(new Label("Float.compareTo result: " + result)); + /// hi.show(); /// ``` /// /// #### Parameters @@ -1467,7 +1472,12 @@ public static int compare(float f1, float f2) { /// integer that would be returned by the call: /// /// ```java - /// new Double(d1).compareTo(new Double(d2)) + /// double d1 = 1.5; + /// double d2 = 2.5; + /// int result = new Double(d1).compareTo(new Double(d2)); + /// Form hi = new Form("MathUtil.compare(double)", BoxLayout.y()); + /// hi.add(new Label("Double.compareTo result: " + result)); + /// hi.show(); /// ``` /// /// #### Parameters 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 3ca75d7a3f..2a816edffc 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 @@ -160,8 +160,8 @@ private void bindGlobals(Interpreter interpreter, PlaygroundContext context) thr interpreter.set("theme", context.getTheme()); interpreter.set("hostForm", context.getHostForm()); interpreter.set("previewRoot", context.getPreviewRoot()); - interpreter.set("display", Display.getInstance()); - interpreter.set("uiManager", UIManager.getInstance()); + interpreter.set("display", resolveDisplayBinding()); + interpreter.set("uiManager", resolveUiManagerBinding()); interpreter.set("FontImage", FontImage.class); interpreter.set("CN", com.codename1.ui.CN.class); interpreter.set("BoxLayout", BoxLayout.class); @@ -179,6 +179,26 @@ private void bindGlobals(Interpreter interpreter, PlaygroundContext context) thr namespace.importClass("com.codenameone.playground.PlaygroundContext"); } + /// Returns the Display singleton, or the Display class itself when no + /// implementation is bootstrapped (headless snippet validator). The CLI + /// validator only cares about parse/lex errors, so a Class fallback is + /// enough -- the script never actually runs against the binding. + private Object resolveDisplayBinding() { + try { + return Display.getInstance(); + } catch (Throwable ex) { + return Display.class; + } + } + + private Object resolveUiManagerBinding() { + try { + return UIManager.getInstance(); + } catch (Throwable ex) { + return UIManager.class; + } + } + private ScriptPlan adaptScript(String script) { String adapted = unwrapSingleTopLevelClass(script); String normalized = adapted == null ? script : adapted; diff --git a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/JavaSnippetToPlaygroundUriHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/JavaSnippetToPlaygroundUriHarness.java index 6202a20d08..f2c6e6ce03 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/JavaSnippetToPlaygroundUriHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/JavaSnippetToPlaygroundUriHarness.java @@ -6,8 +6,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Base64; public final class JavaSnippetToPlaygroundUriHarness { private static final String PREFIX = "/playground/?code="; @@ -22,6 +22,28 @@ public static void main(String[] args) { emitError("UNEXPECTED_ERROR", "No snippet source provided", 1, 1); return; } + currentSource = source; + + // Best-effort: populate Display.impl so BeanShell can report undefined + // identifiers (e.g. "icon" in "setIcon(icon)") instead of NPE-ing inside + // a CN1 constructor. initImpl will throw HeadlessException in CI, but + // Display.impl is assigned before that point, which is enough for name + // resolution. If no port is on the classpath we silently fall back to + // parse-only validation. We mute stdout during init because JavaSEPort + // prints "Retina Scale: ..." which would corrupt the harness output. + java.io.PrintStream savedOut = System.out; + try { + System.setOut(new java.io.PrintStream(new java.io.OutputStream() { + public void write(int b) { + } + })); + try { + com.codename1.ui.Display.init(null); + } catch (Throwable ignored) { + } + } finally { + System.setOut(savedOut); + } PlaygroundContext context = new PlaygroundContext(null, null, null, new PlaygroundContext.Logger() { public void log(String message) { @@ -40,12 +62,35 @@ public void log(String message) { int column = diagnostic == null ? 1 : Math.max(1, diagnostic.column); String message = diagnostic == null ? "Script execution failed" : diagnostic.message; String errorType = classifyErrorType(message); + // Treat undefined-identifier eval errors as failures. CN1 runtime + // failures under the headless harness (NPE because Display.impl is + // null, HeadlessException, etc.) are not the snippet's fault and + // pass; an "Undefined argument" / "Typed variable declaration" eval + // error is a real symbol problem and fails. + if ("EVAL_ERROR".equals(errorType) && isUndefinedSymbolError(message)) { + emitError(errorType, message, line, column); + return; + } if (!"PARSE_ERROR".equals(errorType) && !"LEXER_ERROR".equals(errorType)) { System.out.println(PREFIX + encodeLikePlayground(source)); return; } emitError(errorType, message, line, column); } catch (Throwable ex) { + // BeanShell parse/lex errors arrive as exceptions and are caught by + // PlaygroundRunner; anything reaching this catch is a runtime/init + // failure that did NOT trip the parser. CN1 classes have static + // initializers that touch Display.impl and blow up under the + // headless harness, but those snippets are still valid playground + // input - emit the URI and let the live playground decide. + if (isHeadlessRuntimeFailure(ex)) { + try { + System.out.println(PREFIX + encodeLikePlayground(currentSource)); + return; + } catch (Throwable ignored) { + // fall through to error emission + } + } String message = ex.getMessage(); if (message == null || message.length() == 0) { message = ex.getClass().getName(); @@ -54,6 +99,39 @@ public void log(String message) { } } + /// Heuristic: any Error subclass (or a wrapper carrying one as its cause) + /// is treated as a non-parse failure. ParseException/TokenMgrException are + /// already handled inside PlaygroundRunner.run, so anything reaching the + /// outer catch is by definition a runtime issue. + private static boolean isHeadlessRuntimeFailure(Throwable ex) { + Throwable t = ex; + while (t != null) { + if (t instanceof Error) { + return true; + } + t = t.getCause(); + } + return false; + } + + private static String currentSource = ""; + + /// BeanShell phrases unresolved identifiers a few different ways depending + /// on context (argument position, lhs of assignment, method call target). + /// We deliberately limit the match to "Undefined argument/variable" - those + /// fire for identifiers like the unbound 'icon' in SpanLabel's old sample. + /// "Class X not found in namespace" is intentionally ignored here: that + /// signals a missing import (e.g. Style, ConnectionRequest), which is the + /// snippet's responsibility to declare at the call site, and not a bug to + /// flag in the JavaDoc source. + private static boolean isUndefinedSymbolError(String message) { + if (message == null) { + return false; + } + return message.indexOf("Undefined argument") >= 0 + || message.indexOf("Undefined variable") >= 0; + } + private static String loadSource(String[] args) throws IOException { if (args.length == 0) { return slurp(System.in); @@ -70,7 +148,7 @@ private static String loadSource(String[] args) throws IOException { } private static String slurp(InputStream input) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); StringBuilder out = new StringBuilder(); String line; boolean first = true; @@ -84,13 +162,15 @@ private static String slurp(InputStream input) throws IOException { return out.toString(); } - private static String encodeLikePlayground(String source) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - CN1Playground playground = new CN1Playground(); - Method method = CN1Playground.class.getDeclaredMethod("encodeSharedScript", String.class); - method.setAccessible(true); - Object encoded = method.invoke(playground, source); - return encoded == null ? "" : encoded.toString(); + // Mirrors CN1Playground.encodeSharedScript: URL-safe Base64 without padding. + // Implemented with java.util.Base64 so the headless harness does not require + // an initialized CN1 Display (CN1's Base64 transitively touches Display.impl). + private static String encodeLikePlayground(String source) { + if (source == null || source.isEmpty()) { + return ""; + } + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(source.getBytes(StandardCharsets.UTF_8)); } private static String classifyErrorType(String message) { diff --git a/scripts/java-snippet-to-playground-uri.sh b/scripts/java-snippet-to-playground-uri.sh index c74d965738..cc9c0852ea 100755 --- a/scripts/java-snippet-to-playground-uri.sh +++ b/scripts/java-snippet-to-playground-uri.sh @@ -4,8 +4,10 @@ set -euo pipefail ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" BUILD_DIR="${TMPDIR:-/tmp}/cn1-snippet-cli" CLASSES_DIR="$BUILD_DIR/classes" +LIBS_DIR="$BUILD_DIR/libs" SOURCES_FILE="$BUILD_DIR/sources.txt" STAMP_FILE="$BUILD_DIR/.compiled.ok" +ZIPSUPPORT_SRC="$ROOT_DIR/scripts/cn1playground/cn1libs/ZipSupport/jars/main.zip" usage() { cat <<'USAGE' @@ -67,7 +69,16 @@ else cat > "$TMP_INPUT" fi -mkdir -p "$BUILD_DIR" "$CLASSES_DIR" +mkdir -p "$BUILD_DIR" "$CLASSES_DIR" "$LIBS_DIR" + +# Unpack the ZipSupport cn1lib once so net.sf.zipme.* is on the classpath for +# PlaygroundProjectExporter (and any other playground sources that depend on it). +if [[ -f "$ZIPSUPPORT_SRC" ]] && [[ "$ZIPSUPPORT_SRC" -nt "$LIBS_DIR/.unpacked" ]]; then + rm -rf "$LIBS_DIR" + mkdir -p "$LIBS_DIR" + (cd "$LIBS_DIR" && unzip -q -o "$ZIPSUPPORT_SRC") + touch "$LIBS_DIR/.unpacked" +fi # Build a javac source list from repository sources, excluding BeanShell desktop/classpath files # that are not needed by the CN1 playground runtime. @@ -102,8 +113,24 @@ if [[ -f "$STAMP_FILE" ]]; then fi if [[ "$rebuild" == "true" ]]; then - javac -encoding UTF-8 -d "$CLASSES_DIR" @"$SOURCES_FILE" >/dev/null 2>&1 + javac -encoding UTF-8 -cp "$LIBS_DIR" -d "$CLASSES_DIR" @"$SOURCES_FILE" >/dev/null 2>&1 touch "$STAMP_FILE" fi -java -cp "$CLASSES_DIR" com.codenameone.playground.JavaSnippetToPlaygroundUriHarness --file "$TMP_INPUT" +# If a locally-built JavaSE port jar is available we prepend it to the classpath +# so Display.init(null) can populate Display.impl. With impl bound, BeanShell +# can detect undefined identifiers (e.g. setIcon(icon) when 'icon' is not +# declared) instead of falling over on the first CN1 method call. Without the +# jar we fall back to parse-only validation -- harmless for CI, useful locally. +JAVASE_JAR="" +JAVASE_JAR_GLOB="${HOME}/.m2/repository/com/codenameone/codenameone-javase/8.0-SNAPSHOT/codenameone-javase-8.0-SNAPSHOT.jar" +if [[ -f "$JAVASE_JAR_GLOB" ]]; then + JAVASE_JAR="$JAVASE_JAR_GLOB" +fi + +CP="$CLASSES_DIR:$LIBS_DIR" +if [[ -n "$JAVASE_JAR" ]]; then + CP="$JAVASE_JAR:$CP" +fi + +java -Djava.awt.headless=true -cp "$CP" com.codenameone.playground.JavaSnippetToPlaygroundUriHarness --file "$TMP_INPUT" diff --git a/scripts/java-snippet-validation-exclusions.jsonl b/scripts/java-snippet-validation-exclusions.jsonl index e69de29bb2..cc1c9a7f8b 100644 --- a/scripts/java-snippet-validation-exclusions.jsonl +++ b/scripts/java-snippet-validation-exclusions.jsonl @@ -0,0 +1,4 @@ +{"sourceFile": "CodenameOne/src/com/codename1/ui/layouts/GroupLayout.java", "snippetIndex": 2, "reason": "Method-call signature illustration for the surrounding add(...) JavaDoc; uses an undeclared 'component' placeholder by design and is not intended to run in the playground."} +{"sourceFile": "CodenameOne/src/com/codename1/ui/layouts/GroupLayout.java", "snippetIndex": 3, "reason": "Method-call signature illustration for the surrounding add(...) JavaDoc; uses an undeclared 'component' placeholder by design and is not intended to run in the playground."} +{"sourceFile": "CodenameOne/src/com/codename1/ui/layouts/GroupLayout.java", "snippetIndex": 4, "reason": "Method-call signature illustration for the surrounding add(...) JavaDoc; uses an undeclared 'component' placeholder by design and is not intended to run in the playground."} +{"sourceFile": "CodenameOne/src/com/codename1/ui/layouts/LayoutStyle.java", "snippetIndex": 1, "reason": "Method-call signature illustration for getPreferredGap; uses undeclared 'component1', 'component2', 'parent' placeholders and is not intended to run in the playground."}