From 62731d4bd7797796215d135afd50f0351cb5b6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Sat, 1 Aug 2026 01:42:05 +0200 Subject: [PATCH 01/28] Disable the bundled Vue.js plugin in the test sandbox Since 2025.3, intellijIdea() bundles what used to be Ultimate-only plugins, none of which this plugin depends on or asked for. The Vue.js plugin's VueLspServerSupportProvider intermittently threw ExceptionInInitializerError during lazy init inside the headless test sandbox; doHighlighting() (used by several feature tests) touches every registered extension point, so the resulting logged error failed whichever test happened to be running at the time -- not a real regression, and not always the same test. Confirmed by two green CI runs against d73a807's test sources and two failures after, on unrelated commits that never touched plugin code. --- plugin/build.gradle.kts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index bf7d3a9..f214013 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -1,5 +1,6 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask plugins { id("org.jetbrains.kotlin.jvm") @@ -35,6 +36,15 @@ dependencies { } } +// Since 2025.3, intellijIdea() bundles what used to be Ultimate-only plugins (this +// plugin never asked for any of them). The Vue.js plugin's VueLspServerSupportProvider +// intermittently throws during lazy init in a headless test sandbox, and doHighlighting() +// (used by feature tests) triggers every registered extension point -- so the logged +// error fails whichever test happens to be running at the time, not a real regression. +tasks.named("prepareTestSandbox") { + disabledPlugins.add("org.jetbrains.plugins.vue") +} + // The project-level IntelliJ Platform Gradle Plugin extension: distinct from the // dependencies-scoped `intellijPlatform { }` block above (a different receiver type, // `IntelliJPlatformExtension` vs. `IntelliJPlatformDependenciesExtension`) despite the From 828bd73ff06fdcb6d54edd746c80cfd0d89ac80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:04:58 +0200 Subject: [PATCH 02/28] Fix findings from wiring up verifyPluginProjectConfiguration The check reads sourceCompatibility from :plugin:compileJava's module, not the platform-version-relevant Kotlin code, and IntelliJ's Gradle sync does not evaluate the configureEach {} block that already sets release = 8 -- so without an explicit java {} block on each Java-8 module, the IDE assumed the project default (21) and its inspections suggested syntax those modules can't compile. Also modernizes the mavenExt source set and jar task declarations, replacing the deprecated by ...creating / by ...registering Kotlin DSL delegates with the plain create()/register() calls the Gradle Kotlin DSL now recommends (behavior unchanged), and gives the mavenExtJar task the description every other custom task in this file already has. --- agent/hook/build.gradle.kts | 7 +++++++ agent/instrument/build.gradle.kts | 11 +++++++++++ agent/samples/build.gradle.kts | 7 +++++++ gradle.properties | 6 ++++++ plugin/build.gradle.kts | 11 +++++++++-- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/agent/hook/build.gradle.kts b/agent/hook/build.gradle.kts index 3f5682f..063fc7f 100644 --- a/agent/hook/build.gradle.kts +++ b/agent/hook/build.gradle.kts @@ -14,6 +14,13 @@ plugins { group = "cz.loplex.bsh" description = "Bootstrap-loaded hook invoked by the instrumented BeanShell interpreter" +// See agent/instrument/build.gradle.kts for why sourceCompatibility is declared here too, +// alongside `release`: IntelliJ's Gradle sync doesn't evaluate `configureEach {}` blocks. +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + tasks.withType().configureEach { options.release.set(8) options.encoding = "UTF-8" diff --git a/agent/instrument/build.gradle.kts b/agent/instrument/build.gradle.kts index f0922ff..368080d 100644 --- a/agent/instrument/build.gradle.kts +++ b/agent/instrument/build.gradle.kts @@ -44,6 +44,17 @@ dependencies { // Java 8: the agent loads into whatever JVM the host library runs on, so the floor is set // as low as the tooling allows. +// +// Both lines matter: `release` is what javac actually enforces (rejects newer bytecode AND +// newer source syntax); `sourceCompatibility`/`targetCompatibility` is what IntelliJ's Gradle +// sync reads to set the module's language level -- it does not evaluate `configureEach {}` +// blocks, so without this line the IDE assumes the project default (21) and its inspections +// suggest syntax this module can't actually compile. +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + tasks.withType().configureEach { options.release.set(8) options.encoding = "UTF-8" diff --git a/agent/samples/build.gradle.kts b/agent/samples/build.gradle.kts index a73a291..1e30f9d 100644 --- a/agent/samples/build.gradle.kts +++ b/agent/samples/build.gradle.kts @@ -21,6 +21,13 @@ dependencies { implementation(libs.bsh) } +// See agent/instrument/build.gradle.kts for why sourceCompatibility is declared here too, +// alongside `release`: IntelliJ's Gradle sync doesn't evaluate `configureEach {}` blocks. +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + tasks.withType().configureEach { options.release.set(8) options.encoding = "UTF-8" diff --git a/gradle.properties b/gradle.properties index e587363..73d6e4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,9 @@ org.gradle.caching = true # while the Kotlin plugin code targets JVM 21. This per-jar target mix is safe — the agent never # runs alongside the Kotlin classes in the foreign JVM — so downgrade the consistency check. kotlin.jvm.target.validation.mode = warning + +# VerifyPluginProjectConfigurationTask reads Java sourceCompatibility from the :plugin:compileJava +# task specifically (see its source), which is the Java-8-targeted debug agent above, not the +# Kotlin plugin code the platform version check actually cares about. Mute that one false positive; +# every other configuration check this task runs still reports normally. +org.jetbrains.intellij.platform.verifyPluginProjectConfigurationMutedMessages = Java sourceCompatibility too low for target platform diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index f214013..a41add5 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -45,6 +45,12 @@ tasks.named("prepareTestSandbox") { disabledPlugins.add("org.jetbrains.plugins.vue") } +// Cheap, no-IDE-download sanity check (plugin.xml since-build, Java/Kotlin compatibility +// levels, stray Kotlin stdlib/coroutines deps, ...) -- not wired to `check` by default. +tasks.named("check") { + dependsOn("verifyPluginProjectConfiguration") +} + // The project-level IntelliJ Platform Gradle Plugin extension: distinct from the // dependencies-scoped `intellijPlatform { }` block above (a different receiver type, // `IntelliJPlatformExtension` vs. `IntelliJPlatformDependenciesExtension`) despite the @@ -124,7 +130,7 @@ tasks.named("buildPlugin") { // It rewrites the inline script in the POM model (with the IDE-instrumented text) and // adds the debug-agent callback jar as a system-scoped plugin dependency. Compiled for // Java 8 so it loads in any Maven JVM; it never reaches the plugin's own classpath. -val mavenExt: SourceSet by sourceSets.creating +val mavenExt: SourceSet = sourceSets.create("mavenExt") dependencies { "mavenExtCompileOnly"("org.apache.maven:maven-core:3.6.3") @@ -148,7 +154,8 @@ tasks.named("compileJava") { options.release.set(8) } -val mavenExtJar by tasks.registering(Jar::class) { +val mavenExtJar = tasks.register("mavenExtJar") { + description = "Packages the Maven core extension that rewrites inline BeanShell scripts for debugging" archiveBaseName.set("bsh-maven-ext") archiveVersion.set("") from(mavenExt.output.classesDirs) From bd273e605cec0604ac943d2e18c78f7fd8d66d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Sat, 1 Aug 2026 22:21:25 +0200 Subject: [PATCH 03/28] Rewrite the Marketplace description and add a preview task The old description was three bullet points from before the debugger, Java interoperability, and Maven injection existed. renderMarketplaceDescription lets the new one be reviewed as rendered HTML without publishing anything. --- plugin/build.gradle.kts | 50 ++++++++++++ plugin/src/main/resources/META-INF/plugin.xml | 78 ++++++++++++++++++- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index a41add5..93a4cbd 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -1,6 +1,7 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask +import javax.xml.parsers.DocumentBuilderFactory plugins { id("org.jetbrains.kotlin.jvm") @@ -51,6 +52,55 @@ tasks.named("check") { dependsOn("verifyPluginProjectConfiguration") } +// Renders plugin.xml's (the Marketplace listing text) as a standalone HTML +// file, so it can be reviewed in a browser -- exactly as JetBrains Marketplace will show it -- +// without publishing anything. Not wired to `build`; run it on demand after editing the +// description. +tasks.register("renderMarketplaceDescription") { + group = "documentation" + description = "Renders plugin.xml's as a standalone HTML file for previewing the Marketplace listing" + + val pluginXml = layout.projectDirectory.file("src/main/resources/META-INF/plugin.xml") + val outputFile = layout.buildDirectory.file("marketplace-description.html") + inputs.file(pluginXml) + outputs.file(outputFile) + + doLast { + val description = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(pluginXml.asFile) + .getElementsByTagName("description") + .item(0) + .textContent + .trim() + + val html = """ + | + | + | + | + |BeanShell Language Support -- Marketplace description preview + | + | + | + |$description + | + | + | + """.trimMargin() + + val file = outputFile.get().asFile + file.parentFile.mkdirs() + file.writeText(html) + logger.lifecycle("Marketplace description preview written to file://${file.absolutePath}") + } +} + // The project-level IntelliJ Platform Gradle Plugin extension: distinct from the // dependencies-scoped `intellijPlatform { }` block above (a different receiver type, // `IntelliJPlatformExtension` vs. `IntelliJPlatformDependenciesExtension`) despite the diff --git a/plugin/src/main/resources/META-INF/plugin.xml b/plugin/src/main/resources/META-INF/plugin.xml index 7726a2f..1069c2b 100644 --- a/plugin/src/main/resources/META-INF/plugin.xml +++ b/plugin/src/main/resources/META-INF/plugin.xml @@ -13,12 +13,82 @@ BeanShell scripts (.bsh).
+ Language support for BeanShell (.bsh) scripts: a full syntax + tree, code intelligence, running, and a source-level debugger — plus BeanShell + recognition inside Maven pom.xml configuration and XML in general. +

+ The core runs in any IntelliJ-based IDE (IDEA, WebStorm, PyCharm, CLion, …); + Java-aware features — navigation into Java code, variable type inference and JVM-debugger + attach — light up in IntelliJ IDEA and other IDEs that bundle the Java plugin. +

+ The language model follows the BeanShell 2.0b6 + grammar, which is the version published to Maven Central and bundled with the plugin for + running scripts out of the box. +

+ Source, issue tracker and full documentation: + github.com/loplex/beanshell-debug-tools. +

+ Syntax highlighting and Structure view +

+ Editing
  • Syntax highlighting with a configurable color scheme
  • -
  • Brace matching, code folding and line/block commenting
  • -
  • Structural analysis: unbalanced braces, unterminated strings and comments
  • -
  • Run BeanShell scripts from the editor via a dedicated run configuration
  • +
  • Code folding of blocks, comments and consecutive imports; brace matching
  • +
  • Line/block commenting and an indentation formatter (Reformat Code)
  • +
  • Structure view and breadcrumbs for the enclosing class/method
  • +
  • Read/write highlighting of the variable under the caret
  • +
  • Quick documentation, including a preceding Javadoc-style comment
  • +
  • Live templates (sout, fori, iter, …) and postfix templates (.sout, .if, .while)
  • +
  • Surround With (if, while, try/catch) and TODO highlighting
  • +
+ Code intelligence +
    +
  • A full AST parser (recursive descent with backtracking) that mirrors the BeanShell grammar and reports syntax errors as you type
  • +
  • Go to Declaration, Find Usages and Rename for methods, classes, typed and untyped variables, and parameters — methods and classes also resolve across the project's .bsh files
  • +
  • Code completion, parameter info and inlay parameter-name hints at call sites
  • +
  • Inspections with quick fixes: unused variable/parameter, unreachable code, and an opt-in unresolved method call check
  • +
  • Introduce Variable intention and Go to Symbol
  • +
+ Code completion for keywords and in-scope names +

+ Inspection quick-fix for an unused variable +

+ Java interoperability (requires the Java plugin — optional) +
    +
  • Ctrl+Click into Java: class names (FQN, java.lang.*, imported), and members reached through static type propagation across a chain, e.g. report.append("x").append("y") or list.get(0)
  • +
  • BeanShell class members: greeter.greet() navigates to the greet method of a Greeter class declared in the script
  • +
+ Quick documentation into Java on a chained member +

+ Running +
    +
  • A BeanShell run configuration; right-click a .bsh file to run it
  • +
  • The interpreter is bundled (org.apache-extras.beanshell:bsh:2.0b6), so scripts run with no setup; the classpath is overridable per configuration
  • +
  • Uses the project JDK when available (falls back to the IDE runtime)
  • +
+ Debugging +
    +
  • A source-level debugger for .bsh files: line breakpoints, the call stack, Step Over/Into/Out, and Run to Cursor
  • +
  • A variables view that expands nested objects, collections, maps and arrays, plus Watches, the Evaluate dialog, and Set Value on a variable — all evaluated by the real interpreter in the selected frame
  • +
  • Two instrumentation mechanisms, chosen per run configuration: a JVM agent that leaves the script on disk untouched (default), or rewriting the script, which needs no agent jar and no JVM flag but shows one frame and cannot evaluate
  • +
  • With the Java plugin present, breakpoints in the Java code called from a script are honored by a companion Java (JDWP) debug session
  • +
+ Debugger: variables and console at a breakpoint +

+ File recognition & injection +
    +
  • Files with the .bsh extension
  • +
  • Extensionless scripts whose shebang launches BeanShell, including the self-executing polyglot (#!/bin/shexec java bsh.Interpreter "$0")
  • +
  • Inline scripts in the <configuration> of several Maven plugins (beanshell-maven-plugin, maven-enforcer-plugin, build-helper-maven-plugin — the list is easy to extend)
  • +
  • Any XML element preceded by a <!--language=BeanShell--> comment
  • +
+ BeanShell injected into pom.xml +

+ Known limitations +
    +
  • The parser targets the 2.0b6 grammar; 3.0-only syntax (**, ??, <=>, triple-quoted strings, word operators) is tokenized but not all of it is parsed
  • +
  • Java navigation is static: it follows types evident in the code (typed variable/parameter, = new Type(), class names) — generic element types, array indexing and runtime-only types are not inferred
  • +
  • On JDK 9+, BeanShell 2.0b6 cannot reflectively access some JDK-internal iterators, so list.iterator().next() / for (x : list) may fail at runtime — a property of the interpreter, not the plugin
]]>
From 96ff935f5d1dfe93ca225c04ff7963cab5d0b551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Sun, 2 Aug 2026 18:34:10 +0200 Subject: [PATCH 04/28] Bump mavenExt's maven-core and plexus-utils past their flagged CVEs Both were compile-only/test-only already (never bundled into the shipped plugin), but this sandbox previously had no Maven Central access to verify a version bump would still build. maven-core 3.6.3 -> 3.9.16 and plexus-utils 3.3.0 -> 3.6.1 (latest of their respective 3.x lines) compile and test clean, and also drop the transitive guava/maven-shared-utils to non-vulnerable versions while removing the commons-io/commons-lang3 transitives entirely. Verified end-to-end via agent/checks/run-all.sh, including 02-maven-plugin-realm.sh (real mvn process, real plugin realm), now fully green in this environment for the first time. --- plugin/build.gradle.kts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 93a4cbd..9440cc3 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -183,14 +183,14 @@ tasks.named("buildPlugin") { val mavenExt: SourceSet = sourceSets.create("mavenExt") dependencies { - "mavenExtCompileOnly"("org.apache.maven:maven-core:3.6.3") - "mavenExtCompileOnly"("org.codehaus.plexus:plexus-utils:3.3.0") + "mavenExtCompileOnly"("org.apache.maven:maven-core:3.9.16") + "mavenExtCompileOnly"("org.codehaus.plexus:plexus-utils:3.6.1") "mavenExtCompileOnly"("javax.inject:javax.inject:1") // Unit-test the extension's model surgery against the light Maven model (no maven-core). testImplementation(mavenExt.output) - testImplementation("org.apache.maven:maven-model:3.6.3") - testImplementation("org.codehaus.plexus:plexus-utils:3.3.0") + testImplementation("org.apache.maven:maven-model:3.9.16") + testImplementation("org.codehaus.plexus:plexus-utils:3.6.1") } tasks.named("compileMavenExtJava") { From 30c1a64560195d477773fdc7454312b2aecc4df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Sat, 1 Aug 2026 22:22:06 +0200 Subject: [PATCH 05/28] Apply mechanical Java modernization fixes from an IDE inspection sweep Diamond operators, unnecessary boxing/unboxing, enhanced for-loops, try-with-resources, NIO Files-based stream construction, StandardCharsets, redundant throws/casts/null-checks, and raw-type generics -- all mechanical, behavior-preserving quick fixes reported by an IntelliJ inspection export. --- .../main/java/cz/loplex/bsh/hook/BshHook.java | 108 +++++++--------- .../java/cz/loplex/bsh/hook/DapChannel.java | 120 +++++++++--------- .../main/java/cz/loplex/bsh/hook/Json.java | 15 +-- .../cz/loplex/bsh/hook/NativeChannel.java | 12 +- .../cz/loplex/bsh/agent/BshAgentMain.java | 9 +- agent/samples/src/main/java/DebugHost.java | 38 +++--- .../bsh/debug/agent/BshDebugAgent.java | 8 +- .../mavenext/BshMavenDebugParticipant.java | 10 +- 8 files changed, 147 insertions(+), 173 deletions(-) diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java index 89e1ab7..451a941 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java @@ -4,13 +4,15 @@ import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -134,7 +136,7 @@ public final class BshHook { * own right: {@code BSHBlock} would add a stop on the {@code {} of every braced body, and * {@code BSHSwitchLabel} is a {@code case} label, not a statement. */ - private static final Set NEVER_REPORTED = new HashSet(Arrays.asList( + private static final Set NEVER_REPORTED = new HashSet<>(Arrays.asList( "BSHBlock", "BSHSwitchLabel")); @@ -254,10 +256,10 @@ private static final class ThreadState { /** Commands the reader thread has handed to this thread, in arrival order. */ final BlockingQueue mailbox = - new LinkedBlockingQueue(); + new LinkedBlockingQueue<>(); /** Objects the IDE may expand, valid only for this thread's current stop. */ - final Map handles = new HashMap(); + final Map handles = new HashMap<>(); /** The frames of this thread's current stop, innermost first. Empty while running. */ Object[] frames = new Object[0]; @@ -285,7 +287,7 @@ private static final class ThreadState { /** Live thread states by protocol id, for the reader thread to dispatch into. */ private static final Map threadsById = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); /** * This thread's state, created on first use. @@ -293,7 +295,7 @@ private static final class ThreadState { *

A {@code ThreadLocal} rather than a lookup by {@code Thread.currentThread()} because * {@link #onEval} consults it on every instrumented node — the hottest path in the agent. */ - private static final ThreadLocal STATE = new ThreadLocal(); + private static final ThreadLocal STATE = new ThreadLocal<>(); private static final AtomicInteger nextThreadId = new AtomicInteger(1); @@ -313,7 +315,7 @@ private static final class ThreadState { * overflowed. It stays set for the whole of {@link #report}, so everything served while * suspended is covered, including an expression that calls a script method. */ - private static final ThreadLocal REPORTING = new ThreadLocal(); + private static final ThreadLocal REPORTING = new ThreadLocal<>(); /** * Serialises writes to the socket, and nothing else. @@ -545,22 +547,24 @@ private static boolean isStatement(Object node) throws Exception { if (index < 0) { return false; } - if (IF_STATEMENT.equals(parentName) || SWITCH_STATEMENT.equals(parentName)) { - return index >= 1; - } - if (FOR_STATEMENT.equals(parentName) || ENHANCED_FOR_STATEMENT.equals(parentName)) { - return index == count - 1; - } - if (WHILE_STATEMENT.equals(parentName)) { - return isDoStatement(parent) ? index == 0 : index == count - 1; + switch (parentName) { + case IF_STATEMENT: + case SWITCH_STATEMENT: + return index >= 1; + case FOR_STATEMENT: + case ENHANCED_FOR_STATEMENT: + return index == count - 1; + case WHILE_STATEMENT: + return isDoStatement(parent) ? index == 0 : index == count - 1; + default: + return false; } - return false; } /** Identity search: nodes have no usable equals(), and the same subtree never repeats. */ private static int indexOfChild(Object parent, Object child, int count) throws Exception { for (int i = 0; i < count; i++) { - if (nodeGetChild.invoke(parent, Integer.valueOf(i)) == child) { + if (nodeGetChild.invoke(parent, i) == child) { return i; } } @@ -612,15 +616,15 @@ private static boolean shouldReport(ThreadState state, String sourceFile, int li if (configured == null || state.runMode != MODE_RUN) { return true; } - List files = configured.get(Integer.valueOf(line)); + List files = configured.get(line); if (files == null) { return false; } if (sourceFile == null) { return false; } - for (int i = 0; i < files.size(); i++) { - if (pathsMatch(sourceFile, files.get(i))) { + for (String file : files) { + if (pathsMatch(sourceFile, file)) { return true; } } @@ -707,7 +711,7 @@ private static void readerLoop() { default: break; } - ThreadState target = threadsById.get(Integer.valueOf(command.threadId)); + ThreadState target = threadsById.get(command.threadId); if (target == null) { // A command for a thread that has exited. Dropping it is right: there is nobody // to answer for it, and the client will have been told the thread is gone. @@ -820,14 +824,10 @@ private static void sessionLost(IOException ex) { /** Replaces the breakpoint set with the one the client just sent. */ private static void applyBreakpoints(DebugChannel.Command command) { - Map> parsed = new HashMap>(); + Map> parsed = new HashMap<>(); for (int i = 0; i < command.breakpointLines.length; i++) { - Integer key = Integer.valueOf(command.breakpointLines[i]); - List files = parsed.get(key); - if (files == null) { - files = new ArrayList(2); - parsed.put(key, files); - } + Integer key = command.breakpointLines[i]; + List files = parsed.computeIfAbsent(key, k -> new ArrayList<>(2)); files.add(command.breakpointFiles[i]); } // Published as a whole so a concurrent shouldReport() never sees a half-built map. @@ -873,29 +873,20 @@ private static String[] readSourcePrefixes(String path) { if (path == null || path.trim().isEmpty()) { return null; } - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8")); - List lines = new ArrayList(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(Files.newInputStream(Paths.get(path)), StandardCharsets.UTF_8))) { + List lines = new ArrayList<>(); String line; while ((line = reader.readLine()) != null) { if (!line.isEmpty()) { lines.add(line); } } - return lines.isEmpty() ? null : lines.toArray(new String[lines.size()]); + return lines.isEmpty() ? null : lines.toArray(new String[0]); } catch (IOException ex) { System.err.println("[bsh-agent] cannot read " + SOURCE_PREFIXES_FILE_PROPERTY + "='" + path + "', reporting every source: " + ex); return null; - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException ignored) { - // Nothing useful to do about a failed close on a file we only read. - } - } } } @@ -931,7 +922,7 @@ private static void report(ThreadState state, int line, String sourceFile, Objec state.frames = frames; state.interpreter = interpreter; try { - List reported = new ArrayList(frames.length); + List reported = new ArrayList<>(frames.length); for (int i = 0; i < frames.length; i++) { // Frame 0 sits at the statement being reported; every outer frame sits at the call // site recorded by the frame below it. Reading getInvocationLine() off the frame @@ -979,11 +970,7 @@ private static boolean ensureConnected() { + port + " (" + ex + "); aborting"); System.exit(EXIT_DEBUG_UNAVAILABLE); } - Thread reader = new Thread(new Runnable() { - public void run() { - readerLoop(); - } - }, "bsh-agent-reader"); + Thread reader = new Thread(BshHook::readerLoop, "bsh-agent-reader"); reader.setDaemon(true); reader.start(); } @@ -1038,7 +1025,7 @@ private static ThreadState stateFor() { Thread current = Thread.currentThread(); ThreadState created = new ThreadState(nextThreadId.getAndIncrement(), current.getName()); STATE.set(created); - threadsById.put(Integer.valueOf(created.id), created); + threadsById.put(created.id, created); return created; } @@ -1118,7 +1105,7 @@ private static List collectScopes(ThreadState state, int fra Object namespace = frame(state, frameId); Object global = globalNameSpace(state); boolean hasGlobal = global != null && global != namespace; - List scopes = new ArrayList(2); + List scopes = new ArrayList<>(2); if (namespace != null) { scopes.add(new DebugChannel.Scope("Locals", handleFor(state, namespace))); } @@ -1132,9 +1119,9 @@ private static List collectScopes(ThreadState state, int fra * The children of one handle, each with a handle of its own when it can be expanded further. */ private static List collectVariables(ThreadState state, int handle) { - Object target = state.handles.get(Integer.valueOf(handle)); - List children = new ArrayList(); - List values = new ArrayList(); + Object target = state.handles.get(handle); + List children = new ArrayList<>(); + List values = new ArrayList<>(); try { if (isNameSpace(target)) { collectNamespace(target, children, values); @@ -1155,7 +1142,7 @@ private static List collectVariables(ThreadState state, i } catch (Throwable ignored) { // Send whatever was gathered; an unreadable object is not worth failing the session. } - List variables = new ArrayList(children.size()); + List variables = new ArrayList<>(children.size()); for (int i = 0; i < children.size(); i++) { String[] entry = children.get(i); variables.add(new DebugChannel.Variable(entry[0], entry[1], entry[2], @@ -1242,7 +1229,7 @@ private static Outcome assign(ThreadState state, int frameId, int handle, String if (frame(state, frameId) == null) { return Outcome.failed("No frame " + frameId + " at this stop"); } - Object target = state.handles.get(Integer.valueOf(handle)); + Object target = state.handles.get(handle); if (target == null) { return Outcome.failed("This value is no longer available"); } @@ -1407,7 +1394,7 @@ private static String describe(Throwable error, String expression) { */ private static void collectNamespace(Object namespace, List children, List values) throws Exception { - Set seen = new HashSet(); + Set seen = new HashSet<>(); while (namespace != null) { Object names = nameSpaceGetVariableNames.invoke(namespace); if (names instanceof String[]) { @@ -1439,8 +1426,7 @@ private static void collectValue(Object target, List children, List) target).entrySet()) { - Map.Entry entry = (Map.Entry) o; + for (Map.Entry entry : ((Map) target).entrySet()) { add(children, values, String.valueOf(entry.getKey()), entry.getValue()); if (++i >= MAX_CHILDREN) { break; @@ -1551,7 +1537,7 @@ private static Class primitiveType(Object primitive) { } private static boolean isNameSpace(Object candidate) { - return candidate != null && nameSpaceClass != null && nameSpaceClass.isInstance(candidate); + return nameSpaceClass != null && nameSpaceClass.isInstance(candidate); } /** @@ -1565,7 +1551,7 @@ private static boolean isNameSpace(Object candidate) { * Java. */ private static boolean isThis(Object candidate) { - return candidate != null && thisClass != null && thisClass.isInstance(candidate); + return thisClass != null && thisClass.isInstance(candidate); } /** @@ -1609,11 +1595,11 @@ private static Object globalNameSpace(ThreadState state) { private static int handleFor(ThreadState state, Object value) { for (Map.Entry entry : state.handles.entrySet()) { if (entry.getValue() == value) { - return entry.getKey().intValue(); + return entry.getKey(); } } int handle = nextHandle.getAndIncrement(); - state.handles.put(Integer.valueOf(handle), value); + state.handles.put(handle, value); return handle; } diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java index 7c7af2c..701a05c 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java @@ -7,6 +7,7 @@ import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -64,7 +65,7 @@ final class DapChannel implements DebugChannel { private final AtomicInteger nextSeq = new AtomicInteger(1); /** Breakpoints per source path, since DAP replaces a whole source's set at a time. */ - private final Map breakpointsBySource = new ConcurrentHashMap(); + private final Map breakpointsBySource = new ConcurrentHashMap<>(); /** * The stack most recently reported per thread, so {@code stackTrace} can be answered from it. @@ -73,10 +74,10 @@ final class DapChannel implements DebugChannel { * native protocol sends it inline. Remembering it here is what bridges that, and it costs nothing: * it is the same list that was just sent. */ - private final Map> stacks = new ConcurrentHashMap>(); + private final Map> stacks = new ConcurrentHashMap<>(); /** Thread names, for the {@code threads} request, which may arrive at any time. */ - private final Map threadNames = new ConcurrentHashMap(); + private final Map threadNames = new ConcurrentHashMap<>(); /** * DAP request seq per pending hook request id, so a reply can be addressed to the right request. @@ -84,7 +85,7 @@ final class DapChannel implements DebugChannel { *

The hook's request ids and DAP's {@code seq} numbers are different namespaces, and both ends * insist on their own, so one map is unavoidable. */ - private final Map pending = new ConcurrentHashMap(); + private final Map pending = new ConcurrentHashMap<>(); private final AtomicInteger nextRequestId = new AtomicInteger(1); @@ -141,68 +142,67 @@ public void close() { public void sendStopped(int threadId, String threadName, int line, int callDepth, List frames) throws IOException { - boolean firstSighting = threadNames.put(Integer.valueOf(threadId), threadName) == null; - stacks.put(Integer.valueOf(threadId), frames); + boolean firstSighting = threadNames.put(threadId, threadName) == null; + stacks.put(threadId, frames); if (firstSighting) { // DAP clients build their thread list from these; a thread that never announces itself may // not be selectable. Sent before the stop so the client knows the thread it names. - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("reason", "started"); - body.put("threadId", Integer.valueOf(threadId)); + body.put("threadId", threadId); sendEvent("thread", body); } - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); // "breakpoint" vs "step" is the client's cue for how to present the stop. The hook does not // distinguish, and guessing wrong is cosmetic, so the honest generic reason is used. body.put("reason", "pause"); - body.put("threadId", Integer.valueOf(threadId)); + body.put("threadId", threadId); body.put("allThreadsStopped", Boolean.FALSE); sendEvent("stopped", body); } public void sendScopes(int requestId, List scopes) throws IOException { - Pending request = pending.remove(Integer.valueOf(requestId)); + Pending request = pending.remove(requestId); if (request == null) { return; } - List rendered = new ArrayList(); - for (int i = 0; i < scopes.size(); i++) { - Map scope = new LinkedHashMap(); - scope.put("name", scopes.get(i).name); - scope.put("variablesReference", Integer.valueOf(scopes.get(i).handle)); + List rendered = new ArrayList<>(); + for (Scope item : scopes) { + Map scope = new LinkedHashMap<>(); + scope.put("name", item.name); + scope.put("variablesReference", item.handle); // Locals is worth expanding on arrival; Global usually is not, and saying so keeps a // client from opening a large namespace nobody asked about. - scope.put("expensive", Boolean.valueOf(!"Locals".equals(scopes.get(i).name))); + scope.put("expensive", !"Locals".equals(item.name)); rendered.add(scope); } - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("scopes", rendered); sendResponse(request.seq, request.command, true, body, null); } public void sendVariables(int requestId, List variables) throws IOException { - Pending request = pending.remove(Integer.valueOf(requestId)); + Pending request = pending.remove(requestId); if (request == null) { return; } - List rendered = new ArrayList(); - for (int i = 0; i < variables.size(); i++) { - Variable variable = variables.get(i); - Map entry = new LinkedHashMap(); + List rendered = new ArrayList<>(); + for (Variable variable : variables) { + Map entry = new LinkedHashMap<>(); entry.put("name", variable.name); entry.put("value", variable.value); entry.put("type", variable.type); - entry.put("variablesReference", Integer.valueOf(variable.childHandle)); + entry.put("variablesReference", variable.childHandle); rendered.add(entry); } - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("variables", rendered); sendResponse(request.seq, request.command, true, body, null); } public void sendEvaluated(int requestId, boolean setVariable, boolean ok, String value, String type, int childHandle) throws IOException { - Pending request = pending.remove(Integer.valueOf(requestId)); + Pending request = pending.remove(requestId); if (request == null) { return; } @@ -213,11 +213,11 @@ public void sendEvaluated(int requestId, boolean setVariable, boolean ok, String sendResponse(request.seq, request.command, false, null, value); return; } - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); // setVariable's body calls it "value"; evaluate's calls it "result". body.put(setVariable ? "value" : "result", value); body.put("type", type); - body.put("variablesReference", Integer.valueOf(childHandle)); + body.put("variablesReference", childHandle); sendResponse(request.seq, request.command, true, body, null); } @@ -276,7 +276,7 @@ public Command readCommand() throws IOException { } if ("continue".equals(command)) { int threadId = Json.getInt(args, "threadId", lastStoppedThread()); - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("allThreadsContinued", Boolean.FALSE); sendResponse(seq, command, true, body, null); return Command.simple(Command.Kind.RESUME, threadId); @@ -302,7 +302,7 @@ public Command readCommand() throws IOException { } private Command handleInitialize(int seq, String command) throws IOException { - Map capabilities = new LinkedHashMap(); + Map capabilities = new LinkedHashMap<>(); // Only what is actually true. A capability claimed and then not delivered is worse than one // never claimed: the client builds UI for it and the user finds it broken. capabilities.put("supportsConfigurationDoneRequest", Boolean.TRUE); @@ -314,7 +314,7 @@ private Command handleInitialize(int seq, String command) throws IOException { sendResponse(seq, command, true, capabilities, null); // DAP requires this before the client may send breakpoints. The order matters: a client that // has not seen `initialized` will not configure anything. - sendEvent("initialized", new LinkedHashMap()); + sendEvent("initialized", new LinkedHashMap<>()); return Command.simple(Command.Kind.HANDLED, 0); } @@ -324,21 +324,21 @@ private Command handleSetBreakpoints(int seq, String command, Object args) throw List requested = Json.getList(args, "breakpoints"); int[] lines = new int[requested.size()]; - List verified = new ArrayList(); + List verified = new ArrayList<>(); for (int i = 0; i < requested.size(); i++) { lines[i] = Json.getInt(requested.get(i), "line", 0); - Map entry = new LinkedHashMap(); + Map entry = new LinkedHashMap<>(); // Claimed verified without checking: the agent has no parse tree for the file and cannot // know whether a line holds a statement until execution reaches it. Reporting them all as // verified is the honest answer to "I cannot tell", and matches what the native protocol // does -- the IDE decides placement there too. entry.put("verified", Boolean.TRUE); - entry.put("line", Integer.valueOf(lines[i])); + entry.put("line", lines[i]); verified.add(entry); } breakpointsBySource.put(path, lines); - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("breakpoints", verified); sendResponse(seq, command, true, body, null); @@ -362,14 +362,14 @@ private Command handleSetBreakpoints(int seq, String command, Object args) throw } private Command handleThreads(int seq, String command) throws IOException { - List rendered = new ArrayList(); + List rendered = new ArrayList<>(); for (Map.Entry entry : threadNames.entrySet()) { - Map thread = new LinkedHashMap(); + Map thread = new LinkedHashMap<>(); thread.put("id", entry.getKey()); thread.put("name", entry.getValue()); rendered.add(thread); } - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("threads", rendered); sendResponse(seq, command, true, body, null); return Command.simple(Command.Kind.HANDLED, 0); @@ -384,18 +384,18 @@ private Command handleThreads(int seq, String command) throws IOException { */ private Command handleStackTrace(int seq, String command, Object args) throws IOException { int threadId = Json.getInt(args, "threadId", lastStoppedThread()); - List frames = stacks.get(Integer.valueOf(threadId)); - List rendered = new ArrayList(); + List frames = stacks.get(threadId); + List rendered = new ArrayList<>(); if (frames != null) { for (int i = 0; i < frames.size(); i++) { Frame frame = frames.get(i); - Map entry = new LinkedHashMap(); - entry.put("id", Integer.valueOf(threadId * FRAME_ID_STRIDE + i)); + Map entry = new LinkedHashMap<>(); + entry.put("id", threadId * FRAME_ID_STRIDE + i); entry.put("name", frame.name.isEmpty() ? "?" : frame.name); - entry.put("line", Integer.valueOf(frame.line)); - entry.put("column", Integer.valueOf(1)); + entry.put("line", frame.line); + entry.put("column", 1); if (!frame.sourceFile.isEmpty()) { - Map source = new LinkedHashMap(); + Map source = new LinkedHashMap<>(); source.put("path", frame.sourceFile); source.put("name", shortName(frame.sourceFile)); entry.put("source", source); @@ -403,14 +403,14 @@ private Command handleStackTrace(int seq, String command, Object args) throws IO rendered.add(entry); } } - Map body = new LinkedHashMap(); + Map body = new LinkedHashMap<>(); body.put("stackFrames", rendered); - body.put("totalFrames", Integer.valueOf(rendered.size())); + body.put("totalFrames", rendered.size()); sendResponse(seq, command, true, body, null); return Command.simple(Command.Kind.HANDLED, 0); } - private Command handleSetVariable(int seq, String command, Object args) throws IOException { + private Command handleSetVariable(int seq, String command, Object args) { int reference = Json.getInt(args, "variablesReference", 0); String name = Json.getString(args, "name", ""); String value = Json.getString(args, "value", ""); @@ -470,7 +470,7 @@ boolean isConfigured() { private Command register(int seq, String command, Command hookCommand) { int requestId = nextRequestId.getAndIncrement(); - pending.put(Integer.valueOf(requestId), new Pending(seq, command)); + pending.put(requestId, new Pending(seq, command)); return withRequestId(hookCommand, requestId); } @@ -496,8 +496,8 @@ private int lastStoppedThread() { // thread it is exact; with several, a client that means another one says so explicitly. int candidate = 0; for (Integer id : stacks.keySet()) { - if (id.intValue() > candidate) { - candidate = id.intValue(); + if (id > candidate) { + candidate = id; } } return candidate; @@ -517,8 +517,8 @@ private static String shortName(String path) { } private void sendEvent(String event, Map body) throws IOException { - Map message = new LinkedHashMap(); - message.put("seq", Integer.valueOf(nextSeq.getAndIncrement())); + Map message = new LinkedHashMap<>(); + message.put("seq", nextSeq.getAndIncrement()); message.put("type", "event"); message.put("event", event); message.put("body", body); @@ -527,11 +527,11 @@ private void sendEvent(String event, Map body) throws IOExceptio private void sendResponse(int requestSeq, String command, boolean success, Map body, String message) throws IOException { - Map response = new LinkedHashMap(); - response.put("seq", Integer.valueOf(nextSeq.getAndIncrement())); + Map response = new LinkedHashMap<>(); + response.put("seq", nextSeq.getAndIncrement()); response.put("type", "response"); - response.put("request_seq", Integer.valueOf(requestSeq)); - response.put("success", Boolean.valueOf(success)); + response.put("request_seq", requestSeq); + response.put("success", success); response.put("command", command); if (message != null) { response.put("message", message); @@ -543,10 +543,10 @@ private void sendResponse(int requestSeq, String command, boolean success, Map message) throws IOException { - byte[] payload = Json.write(message).getBytes("UTF-8"); + byte[] payload = Json.write(message).getBytes(StandardCharsets.UTF_8); // Content-Length framing, as DAP specifies: the length counts bytes, not characters, which is // why the payload is encoded before it is measured. - byte[] header = ("Content-Length: " + payload.length + "\r\n\r\n").getBytes("UTF-8"); + byte[] header = ("Content-Length: " + payload.length + "\r\n\r\n").getBytes(StandardCharsets.UTF_8); synchronized (writeLock) { out.write(header); out.write(payload); @@ -587,7 +587,7 @@ private Object readMessage() throws IOException { } read += count; } - String text = new String(payload, "UTF-8"); + String text = new String(payload, StandardCharsets.UTF_8); try { return Json.parse(text); } catch (RuntimeException malformed) { diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java index 1003765..f19ebd5 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java @@ -44,7 +44,7 @@ private static void writeTo(StringBuilder sb, Object value) { } else if (value instanceof String) { writeString(sb, (String) value); } else if (value instanceof Boolean) { - sb.append(((Boolean) value).booleanValue() ? "true" : "false"); + sb.append(value); } else if (value instanceof Integer || value instanceof Long) { sb.append(value); } else if (value instanceof Number) { @@ -62,8 +62,7 @@ private static void writeTo(StringBuilder sb, Object value) { } else if (value instanceof Map) { sb.append('{'); boolean first = true; - for (Object entryObject : ((Map) value).entrySet()) { - Map.Entry entry = (Map.Entry) entryObject; + for (Map.Entry entry : ((Map) value).entrySet()) { if (!first) { sb.append(','); } @@ -121,7 +120,7 @@ private static void writeString(StringBuilder sb, String text) { // Control characters must be escaped; a script's own value may well contain them. // Everything else goes out as-is and is UTF-8 encoded by the writer. if (c < 0x20) { - sb.append(String.format("\\u%04x", Integer.valueOf(c))); + sb.append(String.format("\\u%04x", (int) c)); } else { sb.append(c); } @@ -165,14 +164,14 @@ static String getString(Object object, String key, String fallback) { /** A member as a boolean, or [fallback] when absent or not a boolean. */ static boolean getBoolean(Object object, String key, boolean fallback) { Object value = get(object, key); - return value instanceof Boolean ? ((Boolean) value).booleanValue() : fallback; + return value instanceof Boolean ? (Boolean) value : fallback; } /** A member as a list, or an empty list when absent or not an array. */ @SuppressWarnings("unchecked") static List getList(Object object, String key) { Object value = get(object, key); - return value instanceof List ? (List) value : new ArrayList(); + return value instanceof List ? (List) value : new ArrayList<>(); } private static final class Parser { @@ -222,7 +221,7 @@ Object value() { } private Map object() { - Map result = new LinkedHashMap(); + Map result = new LinkedHashMap<>(); position++; // '{' skipWhitespace(); if (peek() == '}') { @@ -252,7 +251,7 @@ private Map object() { } private List array() { - List result = new ArrayList(); + List result = new ArrayList<>(); position++; // '[' skipWhitespace(); if (peek() == ']') { diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java index 71482b2..1ff98f8 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java @@ -80,8 +80,7 @@ public void sendStopped(int threadId, String threadName, int line, int callDepth out.writeInt(line); out.writeInt(callDepth); out.writeInt(frames.size()); - for (int i = 0; i < frames.size(); i++) { - Frame frame = frames.get(i); + for (Frame frame : frames) { out.writeUTF(frame.name); out.writeUTF(frame.sourceFile); out.writeInt(frame.line); @@ -95,9 +94,9 @@ public void sendScopes(int requestId, List scopes) throws IOException { out.writeByte(EVT_SCOPES); out.writeInt(requestId); out.writeInt(scopes.size()); - for (int i = 0; i < scopes.size(); i++) { - out.writeUTF(scopes.get(i).name); - out.writeInt(scopes.get(i).handle); + for (Scope scope : scopes) { + out.writeUTF(scope.name); + out.writeInt(scope.handle); } out.flush(); } @@ -108,8 +107,7 @@ public void sendVariables(int requestId, List variables) throws IOExce out.writeByte(EVT_VARIABLES); out.writeInt(requestId); out.writeInt(variables.size()); - for (int i = 0; i < variables.size(); i++) { - Variable variable = variables.get(i); + for (Variable variable : variables) { out.writeUTF(variable.name); out.writeUTF(variable.value); out.writeUTF(variable.type); diff --git a/agent/instrument/src/main/java/cz/loplex/bsh/agent/BshAgentMain.java b/agent/instrument/src/main/java/cz/loplex/bsh/agent/BshAgentMain.java index 039150a..a289986 100644 --- a/agent/instrument/src/main/java/cz/loplex/bsh/agent/BshAgentMain.java +++ b/agent/instrument/src/main/java/cz/loplex/bsh/agent/BshAgentMain.java @@ -1,11 +1,11 @@ package cz.loplex.bsh.agent; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.instrument.Instrumentation; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.jar.JarFile; @@ -112,15 +112,12 @@ private static boolean publishHookToBootstrap(Instrumentation inst) { try { File extracted = File.createTempFile("bsh-debug-hook", ".jar"); extracted.deleteOnExit(); - OutputStream target = new FileOutputStream(extracted); - try { + try (OutputStream target = Files.newOutputStream(extracted.toPath())) { byte[] buffer = new byte[8192]; int read; while ((read = source.read(buffer)) > 0) { target.write(buffer, 0, read); } - } finally { - target.close(); } inst.appendToBootstrapClassLoaderSearch(new JarFile(extracted)); return true; @@ -145,7 +142,7 @@ private static void retransformLoadedBshClasses(Instrumentation inst) { if (!inst.isRetransformClassesSupported()) { return; } - List> targets = new ArrayList>(); + List> targets = new ArrayList<>(); for (Class candidate : inst.getAllLoadedClasses()) { if (isBshClass(candidate) && inst.isModifiableClass(candidate)) { targets.add(candidate); diff --git a/agent/samples/src/main/java/DebugHost.java b/agent/samples/src/main/java/DebugHost.java index 1b56cc2..5ebcb91 100644 --- a/agent/samples/src/main/java/DebugHost.java +++ b/agent/samples/src/main/java/DebugHost.java @@ -1,18 +1,18 @@ /** * Embeds BeanShell the way a third-party library does, so the agent under test * sees a realistic entry pattern rather than the CLI one. - * + *

* Why this matters: running a script with `java bsh.Interpreter foo.bsh` goes * through Interpreter.run() (Interpreter.java:471). Everything a library does * instead goes through Interpreter.eval(Reader, NameSpace, String) * (Interpreter.java:659). Those are two SEPARATE loops -- an agent that hooks * only one will look like it works in the CLI and do nothing in production, or * vice versa. Scenario 1 below covers eval(); run the CLI runner for the other. - * + *

* Run from the repository root: * ./gradlew :agent:samples:runHost # all scenarios * ./gradlew :agent:samples:runHostWithAgent # the same, instrumented - * + *

* Both tasks set the working directory to scripts/, because the fixtures source() * each other by bare name. To pick a single scenario, append --args='3'. */ @@ -27,6 +27,7 @@ import java.util.Comparator; import java.util.List; +@SuppressWarnings("JavaPrintToLogpoint") public class DebugHost { /** Directory holding the .bsh fixtures. Override with -Dsamples=/path. */ @@ -49,6 +50,7 @@ public static void main(String[] args) throws Exception { } private static Interpreter fresh() { + @SuppressWarnings("UnnecessaryLocalVariable") Interpreter i = new Interpreter(); // Uncomment to see the built-in tracing for comparison with your agent. // Interpreter.TRACE = true; @@ -97,7 +99,7 @@ private static void scenario3_callScriptMethodFromJava() throws EvalError { Interpreter i = fresh(); i.eval("compute(a, b) { inner = a * b; return inner + 1; }"); NameSpace global = i.getNameSpace(); - Object result = global.invokeMethod("compute", new Object[]{6, 7}, i); + Object result = global.invokeMethod("compute", new Object[] { 6 , 7 }, i); System.out.println("[host] compute(6,7) = " + result); } @@ -115,13 +117,14 @@ private static void scenario4_scriptImplementsInterface() throws Exception { + " return this;" + "}"); This scripted = (This) i.eval("makeCmp();"); - Comparator cmp = (Comparator) scripted.getInterface(Comparator.class); + @SuppressWarnings("unchecked") // BeanShell's This.getInterface predates generics + Comparator cmp = (Comparator) scripted.getInterface(Comparator.class); - List words = new ArrayList(); + List words = new ArrayList<>(); words.add("ccc"); words.add("a"); words.add("bb"); - java.util.Collections.sort(words, cmp); // Java drives the script + words.sort(cmp); // Java drives the script System.out.println("[host] sorted = " + words); } @@ -153,17 +156,13 @@ private static void scenario5_callOnOtherThread() throws Exception { b.join(); } - private static Runnable makeJob( - final Interpreter i, final NameSpace ns, final String tag) { - return new Runnable() { - public void run() { - try { - Object r = ns.invokeMethod( - "work", new Object[]{tag, 3}, i); - System.out.println("[host] " + r); - } catch (EvalError e) { - throw new RuntimeException(e); - } + private static Runnable makeJob(Interpreter i, NameSpace ns, String tag) { + return () -> { + try { + Object r = ns.invokeMethod("work", new Object[] { tag, 3 }, i); + System.out.println("[host] " + r); + } catch (EvalError e) { + throw new RuntimeException(e); } }; } @@ -183,8 +182,7 @@ private static void scenario6_scriptedClassAsJavaObject() throws Exception { System.out.println("[host] java class = " + p.getClass().getName()); System.out.println("[host] toString = " + p); // Reflective call straight into the generated shim. - Object d2 = p.getClass().getMethod("distanceSquared", new Class[0]) - .invoke(p, new Object[0]); + Object d2 = p.getClass().getMethod("distanceSquared").invoke(p); System.out.println("[host] distanceSquared = " + d2 + " (expect 169)"); } diff --git a/plugin/src/main/java/cz/loplex/intellij/bsh/debug/agent/BshDebugAgent.java b/plugin/src/main/java/cz/loplex/intellij/bsh/debug/agent/BshDebugAgent.java index 86ff4ee..e6358fb 100644 --- a/plugin/src/main/java/cz/loplex/intellij/bsh/debug/agent/BshDebugAgent.java +++ b/plugin/src/main/java/cz/loplex/intellij/bsh/debug/agent/BshDebugAgent.java @@ -68,7 +68,7 @@ public final class BshDebugAgent { * alive. Losing an entry costs nothing: a thread that never reports again never needs its id, and * one that does gets a fresh id, which the IDE treats as a new thread. */ - private static final Map threadIds = new WeakHashMap(); + private static final Map threadIds = new WeakHashMap<>(); private static final int port; private static boolean disabled; @@ -161,10 +161,10 @@ private static int threadId(Thread thread) { synchronized (threadIds) { Integer existing = threadIds.get(thread); if (existing != null) { - return existing.intValue(); + return existing; } int assigned = threadIds.size() + 1; - threadIds.put(thread, Integer.valueOf(assigned)); + threadIds.put(thread, assigned); return assigned; } } @@ -246,7 +246,7 @@ private static void serveUntilResume(Object namespace) throws IOException { int requestId = in.readInt(); int handle = in.readInt(); Map variables = - handle == NAMESPACE_HANDLE ? readVariables(namespace) : Collections.emptyMap(); + handle == NAMESPACE_HANDLE ? readVariables(namespace) : Collections.emptyMap(); out.writeByte(EVT_VARIABLES); out.writeInt(requestId); out.writeInt(variables.size()); diff --git a/plugin/src/mavenExt/java/cz/loplex/intellij/bsh/mavenext/BshMavenDebugParticipant.java b/plugin/src/mavenExt/java/cz/loplex/intellij/bsh/mavenext/BshMavenDebugParticipant.java index 88ecb9b..06af178 100644 --- a/plugin/src/mavenExt/java/cz/loplex/intellij/bsh/mavenext/BshMavenDebugParticipant.java +++ b/plugin/src/mavenExt/java/cz/loplex/intellij/bsh/mavenext/BshMavenDebugParticipant.java @@ -74,8 +74,7 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti /** Parses the manifest into substitutions grouped by the owning plugin's artifactId. */ private Map> readManifest(String manifest) throws MavenExecutionException { - Map> byArtifact = - new LinkedHashMap>(); + Map> byArtifact = new LinkedHashMap<>(); try { for (String line : Files.readAllLines(Paths.get(manifest), StandardCharsets.UTF_8)) { if (line.isEmpty() || line.charAt(0) == '#') { @@ -90,11 +89,8 @@ private Map> readManifest(String ma String original = new String(Files.readAllBytes(Paths.get(parts[2])), StandardCharsets.UTF_8); String instrumented = new String(Files.readAllBytes(Paths.get(parts[3])), StandardCharsets.UTF_8); - List substitutions = byArtifact.get(artifactId); - if (substitutions == null) { - substitutions = new ArrayList(); - byArtifact.put(artifactId, substitutions); - } + List substitutions = + byArtifact.computeIfAbsent(artifactId, k -> new ArrayList<>()); substitutions.add(new BshScriptRewriter.Substitution(tag, original, instrumented)); } } catch (IOException ex) { From e162398e0caee47142e639521a33668651d55a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Sat, 1 Aug 2026 22:22:35 +0200 Subject: [PATCH 06/28] Apply mechanical Kotlin modernization fixes from an IDE inspection sweep Diamond-equivalent generic inference, multi-dollar string interpolation, property-access syntax, Enum.entries, destructuring, redundant if/when simplification, and moving companion objects that held only constants to top-level file-scope declarations -- all mechanical, behavior-preserving quick fixes reported by an IntelliJ inspection export. --- .../intellij/bsh/BshFileTypeDetector.kt | 10 ++- .../intellij/bsh/BshParserDefinition.kt | 6 +- .../intellij/bsh/debug/BshDebugFrames.kt | 7 +- .../bsh/debug/BshDebugInstrumentation.kt | 2 +- .../loplex/intellij/bsh/debug/BshStepLogic.kt | 3 +- .../BshMavenCreateRunConfigurationAction.kt | 5 +- .../debug/maven/BshMavenRunConfiguration.kt | 5 +- .../bsh/debug/maven/BshMavenSettingsEditor.kt | 2 +- .../intellij/bsh/editor/BshBraceMatcher.kt | 14 ++-- .../bsh/highlight/BshColorSettingsPage.kt | 72 +++++++++---------- .../bsh/injection/BshMavenInjector.kt | 16 ++--- .../BshIntroduceVariableIntention.kt | 14 ++-- .../intellij/bsh/reference/BshJavaResolver.kt | 6 +- .../intellij/bsh/reference/BshScopes.kt | 3 +- .../intellij/bsh/run/BshSettingsEditor.kt | 2 +- .../bsh/template/BshPostfixTemplates.kt | 6 +- .../cz/loplex/intellij/bsh/BshFeaturesTest.kt | 6 +- .../bsh/mavenext/BshScriptRewriterTest.kt | 4 +- 18 files changed, 85 insertions(+), 98 deletions(-) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshFileTypeDetector.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshFileTypeDetector.kt index dcd3334..0c1cb67 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshFileTypeDetector.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshFileTypeDetector.kt @@ -37,10 +37,8 @@ class BshFileTypeDetector : FileTypeRegistry.FileTypeDetector { } override fun getDesiredContentPrefixLength(): Int = 256 - - companion object { - private const val MAX_HEADER_LINES = 6 - private val INTERPRETER = Regex("""\bbsh\.Interpreter\b""") - private val INTERPRETER_NAME = Regex("""\b(?:beanshell|bsh)\b""", RegexOption.IGNORE_CASE) - } } + +private const val MAX_HEADER_LINES = 6 +private val INTERPRETER = Regex("""\bbsh\.Interpreter\b""") +private val INTERPRETER_NAME = Regex("""\b(?:beanshell|bsh)\b""", RegexOption.IGNORE_CASE) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshParserDefinition.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshParserDefinition.kt index 6ea97bc..7598373 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshParserDefinition.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/BshParserDefinition.kt @@ -47,8 +47,6 @@ class BshParserDefinition : ParserDefinition { } override fun createFile(viewProvider: FileViewProvider): PsiFile = BshFile(viewProvider) - - companion object { - val FILE = IFileElementType(BshLanguage) - } } + +val FILE = IFileElementType(BshLanguage) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugFrames.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugFrames.kt index ff9674f..e6de2ea 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugFrames.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugFrames.kt @@ -280,9 +280,9 @@ private class BshValueModifier( * BeanShell literal too. A `toString()` is not generally an expression, so offering `Point@1c2f` * back would hand the user something that cannot even parse; those open empty instead. */ - override fun getInitialValueEditorText(): String? = when { - variable.type == "String" -> quotedLiteral(variable.value) - variable.type in LITERAL_TYPES -> variable.value + override fun getInitialValueEditorText(): String? = when (variable.type) { + "String" -> quotedLiteral(variable.value) + in LITERAL_TYPES -> variable.value else -> null } } @@ -315,6 +315,7 @@ private fun quotedLiteral(value: String): String = buildString(value.length + 2) // Wrapped explicitly rather than passed as a lambda: executeOnPooledThread is overloaded for // Runnable and Callable, and a Kotlin `() -> Unit` fits both. +@Suppress("RedundantSamConstructor") private fun onPooledThread(work: () -> Unit) { ApplicationManager.getApplication().executeOnPooledThread(Runnable { work() }) } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt index d374242..288da91 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt @@ -87,7 +87,7 @@ enum class BshInstrumentationMode( * value written by a different version of the plugin — or edited by hand. Falling back to * [DEFAULT] costs the user their choice; refusing to launch would cost them the session. */ - fun of(name: String?): BshInstrumentationMode = values().firstOrNull { it.name == name } ?: DEFAULT + fun of(name: String?): BshInstrumentationMode = entries.firstOrNull { it.name == name } ?: DEFAULT } } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshStepLogic.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshStepLogic.kt index 8e677fa..d47e4b3 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshStepLogic.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshStepLogic.kt @@ -11,8 +11,7 @@ enum class BshStepMode { RUN, INTO, OVER, OUT } */ object BshStepLogic { fun shouldPause(mode: BshStepMode, stepDepth: Int, currentDepth: Int, atBreakpoint: Boolean): Boolean { - if (atBreakpoint) return true - return when (mode) { + return atBreakpoint || when (mode) { BshStepMode.RUN -> false BshStepMode.INTO -> true // next statement, wherever it is BshStepMode.OVER -> currentDepth <= stepDepth // skip descents into called methods diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenCreateRunConfigurationAction.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenCreateRunConfigurationAction.kt index f8735b7..f02c1f9 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenCreateRunConfigurationAction.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenCreateRunConfigurationAction.kt @@ -38,9 +38,8 @@ class BshMavenCreateRunConfigurationAction : AnAction() { val name = "${mavenProject.mavenId.artifactId ?: "maven"} [${goals.joinToString(" ")}] (bsh)" val settings = runManager.createConfiguration(name, factory) - (settings.configuration as BshMavenRunConfiguration).setRunnerParameters( - MavenRunnerParameters(false, mavenProject.directory, mavenProject.file.name, goals, emptyList(), emptyList()), - ) + (settings.configuration as BshMavenRunConfiguration).runnerParameters = + MavenRunnerParameters(false, mavenProject.directory, mavenProject.file.name, goals, emptyList(), emptyList()) if (RunDialog.editConfiguration(project, settings, "Create BeanShell-Enhanced Maven Configuration")) { runManager.addConfiguration(settings) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt index 56707a3..201f402 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt @@ -115,7 +115,7 @@ class BshMavenRunConfiguration( try { // Materialise a private runner settings on the clone before injecting our properties. val settings = (runnerSettings ?: MavenRunner.getInstance(project).settings).clone() - setRunnerSettings(settings) + runnerSettings = settings val props = LinkedHashMap(settings.mavenProperties) props[BshDebugAgent.PORT_PROPERTY] = server.localPort.toString() @@ -125,7 +125,7 @@ class BshMavenRunConfiguration( val notice = if (mode.toleratesRewriteFallback) BshDebugRunner.REWRITE_FALLBACK_NOTICE else null setUpRewrite(props, prepared, server, notice) ?: return } - settings.setMavenProperties(props) + settings.mavenProperties = props BshMavenDebugSupport.register(environment.executionId, pending) LOG.info("Prepared BeanShell Maven debug for ${prepared.size} script(s) on port ${server.localPort}") @@ -155,6 +155,7 @@ class BshMavenRunConfiguration( server: ServerSocket, ): BshMavenDebugSupport.Pending { val prefixes = BshMavenDebugSupport.writeSourcePrefixes(prepared) + @Suppress("UsePropertyAccessSyntax") settings.setVmOptions( listOfNotNull( settings.vmOptions.takeIf { it.isNotBlank() }, diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenSettingsEditor.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenSettingsEditor.kt index ef959d4..a0b8674 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenSettingsEditor.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenSettingsEditor.kt @@ -20,7 +20,7 @@ import javax.swing.JComponent */ class BshMavenSettingsEditor : SettingsEditor() { - private val instrumentation = ComboBox(BshInstrumentationMode.values()).apply { + private val instrumentation = ComboBox(BshInstrumentationMode.entries.toTypedArray()).apply { renderer = SimpleListCellRenderer.create("") { it.label } } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshBraceMatcher.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshBraceMatcher.kt index bcb8500..67005f6 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshBraceMatcher.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshBraceMatcher.kt @@ -12,12 +12,10 @@ class BshBraceMatcher : PairedBraceMatcher { override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?): Boolean = true override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int): Int = openingBraceOffset - - companion object { - private val PAIRS = arrayOf( - BracePair(BshTokenTypes.LBRACE, BshTokenTypes.RBRACE, true), - BracePair(BshTokenTypes.LPAREN, BshTokenTypes.RPAREN, false), - BracePair(BshTokenTypes.LBRACKET, BshTokenTypes.RBRACKET, false), - ) - } } + +private val PAIRS = arrayOf( + BracePair(BshTokenTypes.LBRACE, BshTokenTypes.RBRACE, true), + BracePair(BshTokenTypes.LPAREN, BshTokenTypes.RPAREN, false), + BracePair(BshTokenTypes.LBRACKET, BshTokenTypes.RBRACKET, false), +) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/highlight/BshColorSettingsPage.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/highlight/BshColorSettingsPage.kt index 14a70e8..df61084 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/highlight/BshColorSettingsPage.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/highlight/BshColorSettingsPage.kt @@ -22,46 +22,44 @@ class BshColorSettingsPage : ColorSettingsPage { override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY override fun getDisplayName(): String = "BeanShell" +} - companion object { - private val DESCRIPTORS = arrayOf( - AttributesDescriptor("Keyword", BshColors.KEYWORD), - AttributesDescriptor("Identifier", BshColors.IDENTIFIER), - AttributesDescriptor("Number", BshColors.NUMBER), - AttributesDescriptor("String", BshColors.STRING), - AttributesDescriptor("Character", BshColors.CHARACTER), - AttributesDescriptor("Comments//Line comment", BshColors.LINE_COMMENT), - AttributesDescriptor("Comments//Block comment", BshColors.BLOCK_COMMENT), - AttributesDescriptor("Comments//Doc comment", BshColors.DOC_COMMENT), - AttributesDescriptor("Operator sign", BshColors.OPERATOR), - AttributesDescriptor("Parentheses", BshColors.PARENTHESES), - AttributesDescriptor("Braces", BshColors.BRACES), - AttributesDescriptor("Brackets", BshColors.BRACKETS), - AttributesDescriptor("Semicolon", BshColors.SEMICOLON), - AttributesDescriptor("Comma", BshColors.COMMA), - AttributesDescriptor("Dot", BshColors.DOT), - AttributesDescriptor("Bad character", BshColors.BAD_CHARACTER), - ) +private val DESCRIPTORS = arrayOf( + AttributesDescriptor("Keyword", BshColors.KEYWORD), + AttributesDescriptor("Identifier", BshColors.IDENTIFIER), + AttributesDescriptor("Number", BshColors.NUMBER), + AttributesDescriptor("String", BshColors.STRING), + AttributesDescriptor("Character", BshColors.CHARACTER), + AttributesDescriptor("Comments//Line comment", BshColors.LINE_COMMENT), + AttributesDescriptor("Comments//Block comment", BshColors.BLOCK_COMMENT), + AttributesDescriptor("Comments//Doc comment", BshColors.DOC_COMMENT), + AttributesDescriptor("Operator sign", BshColors.OPERATOR), + AttributesDescriptor("Parentheses", BshColors.PARENTHESES), + AttributesDescriptor("Braces", BshColors.BRACES), + AttributesDescriptor("Brackets", BshColors.BRACKETS), + AttributesDescriptor("Semicolon", BshColors.SEMICOLON), + AttributesDescriptor("Comma", BshColors.COMMA), + AttributesDescriptor("Dot", BshColors.DOT), + AttributesDescriptor("Bad character", BshColors.BAD_CHARACTER), +) - private val DEMO_TEXT = """ - /** - * Sample BeanShell script. - */ - import javax.swing.*; +private val DEMO_TEXT = """ + /** + * Sample BeanShell script. + */ + import javax.swing.*; - // loosely typed variable - greeting = "Hello, BeanShell"; - count = 0x2A; // 42 - ratio = 3.14f @pow 2; // word operator + // loosely typed variable + greeting = "Hello, BeanShell"; + count = 0x2A; // 42 + ratio = 3.14f @pow 2; // word operator - invoke(String name) { - print("Hi " + name); - return name != null ? name : "world"; - } + invoke(String name) { + print("Hi " + name); + return name != null ? name : "world"; + } - for (int i = 0; i < 3; i++) { - invoke(greeting + i); - } - """.trimIndent() + for (int i = 0; i < 3; i++) { + invoke(greeting + i); } -} +""".trimIndent() diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt index a12ef88..d4a2240 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt @@ -63,10 +63,10 @@ class BshMavenInjector : MultiHostInjector { private fun hasLanguageComment(tag: XmlTag): Boolean { var sibling = tag.prevSibling while (sibling != null) { - when { - sibling is PsiWhiteSpace -> {} - sibling is XmlText && sibling.getText().isBlank() -> {} - sibling is XmlComment -> return LANGUAGE_COMMENT.containsMatchIn(sibling.text) + when (sibling) { + is PsiWhiteSpace -> {} + is XmlText -> if (!sibling.text.isBlank()) return false + is XmlComment -> return LANGUAGE_COMMENT.containsMatchIn(sibling.text) else -> return false } sibling = sibling.prevSibling @@ -94,9 +94,7 @@ class BshMavenInjector : MultiHostInjector { private fun childValue(tag: XmlTag, name: String): String? = tag.findFirstSubTag(name)?.value?.trimmedText - - companion object { - private val LANGUAGE_COMMENT = - Regex("(?i)(?:language|lang)\\s*=\\s*(?:beanshell|bsh)") - } } + +private val LANGUAGE_COMMENT = + Regex("(?i)(?:language|lang)\\s*=\\s*(?:beanshell|bsh)") diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/intention/BshIntroduceVariableIntention.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/intention/BshIntroduceVariableIntention.kt index 296f48d..d1e6a8d 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/intention/BshIntroduceVariableIntention.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/intention/BshIntroduceVariableIntention.kt @@ -52,12 +52,10 @@ class BshIntroduceVariableIntention : IntentionAction { val text = file.text return CANDIDATES.firstOrNull { !Regex("\\b${Regex.escape(it)}\\b").containsMatchIn(text) } ?: "x1" } - - companion object { - private val EXPRESSION_TYPES = setOf( - E.PRIMARY_EXPRESSION, E.BINARY_EXPRESSION, E.UNARY_EXPRESSION, E.TERNARY_EXPRESSION, - E.CAST_EXPRESSION, E.METHOD_INVOCATION, E.ALLOCATION_EXPRESSION, E.LITERAL, - ) - private val CANDIDATES = listOf("x", "y", "z", "value", "result") - } } + +private val EXPRESSION_TYPES = setOf( + E.PRIMARY_EXPRESSION, E.BINARY_EXPRESSION, E.UNARY_EXPRESSION, E.TERNARY_EXPRESSION, + E.CAST_EXPRESSION, E.METHOD_INVOCATION, E.ALLOCATION_EXPRESSION, E.LITERAL, +) +private val CANDIDATES = listOf("x", "y", "z", "value", "result") diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt index 0db6fe9..a626f25 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt @@ -33,10 +33,10 @@ object BshJavaResolver { if (name.contains('.')) return null // an explicit FQN that was not found facade.findClass("java.lang.$name", scope)?.let { return it } - for (imp in imports(context)) { + for ((path, onDemand) in imports(context)) { val candidate = when { - imp.onDemand -> "${imp.path}.$name" - imp.path.substringAfterLast('.') == name -> imp.path + onDemand -> "$path.$name" + path.substringAfterLast('.') == name -> path else -> continue } facade.findClass(candidate, scope)?.let { return it } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshScopes.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshScopes.kt index 07901d7..7e03536 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshScopes.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshScopes.kt @@ -44,7 +44,6 @@ object BshScopes { if (significant.size != 1 || significant[0] !== name.node) return false val assignment = primary.parent ?: return false - if (assignment.node.elementType !== E.ASSIGNMENT) return false - return assignment.node.firstChildNode === primary.node + return assignment.node.elementType === E.ASSIGNMENT && assignment.node.firstChildNode === primary.node } } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt index 6f3ab15..864d1ec 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt @@ -22,7 +22,7 @@ class BshSettingsEditor : SettingsEditor() { private val jrePath = TextFieldWithBrowseButton() private val workingDirectory = TextFieldWithBrowseButton() private val programArguments = RawCommandLineEditor() - private val instrumentation = ComboBox(BshInstrumentationMode.values()).apply { + private val instrumentation = ComboBox(BshInstrumentationMode.entries.toTypedArray()).apply { renderer = SimpleListCellRenderer.create("") { it.label } } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/template/BshPostfixTemplates.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/template/BshPostfixTemplates.kt index 65d4ac6..2351f78 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/template/BshPostfixTemplates.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/template/BshPostfixTemplates.kt @@ -15,9 +15,9 @@ import cz.loplex.intellij.bsh.psi.BshElementTypes as E class BshPostfixTemplateProvider : PostfixTemplateProvider { private val templateSet: Set = setOf( - BshStringPostfixTemplate("sout", "print(expr);", "print(\$expr\$);\$END\$", this), - BshStringPostfixTemplate("if", "if (expr) {...}", "if (\$expr\$) {\n\$END\$\n}", this), - BshStringPostfixTemplate("while", "while (expr) {...}", "while (\$expr\$) {\n\$END\$\n}", this), + BshStringPostfixTemplate("sout", "print(expr);", $$"print($expr$);$END$", this), + BshStringPostfixTemplate("if", "if (expr) {...}", $$"if ($expr$) {\n$END$\n}", this), + BshStringPostfixTemplate("while", "while (expr) {...}", $$"while ($expr$) {\n$END$\n}", this), ) override fun getTemplates(): Set = templateSet diff --git a/plugin/src/test/kotlin/cz/loplex/intellij/bsh/BshFeaturesTest.kt b/plugin/src/test/kotlin/cz/loplex/intellij/bsh/BshFeaturesTest.kt index e2166e1..0507d18 100644 --- a/plugin/src/test/kotlin/cz/loplex/intellij/bsh/BshFeaturesTest.kt +++ b/plugin/src/test/kotlin/cz/loplex/intellij/bsh/BshFeaturesTest.kt @@ -125,19 +125,19 @@ class BshFeaturesTest : BasePlatformTestCase() { fun testSelfExecutingPolyglotHack() { val content = "#!/bin/sh\n" + "// The following hack allows java to reside anywhere in the PATH.\n" + - "//bin/true; exec java bsh.Interpreter \"\$0\" \"\$@\"\n" + + $$"//bin/true; exec java bsh.Interpreter \"$0\" \"$@\"\n" + "print(1);" assertEquals(BshFileType, detect(content)) } fun testSelfExecutingPolyglotWithBashShebang() { - val content = "#!/bin/bash\n//bin/true; exec java bsh.Interpreter \"\$0\" \"\$@\"\nprint(1);" + val content = $$"#!/bin/bash\n//bin/true; exec java bsh.Interpreter \"$0\" \"$@\"\nprint(1);" assertEquals(BshFileType, detect(content)) } fun testSelfExecutingPolyglotWithArbitraryShebang() { // The shebang target is irrelevant; only the bsh.Interpreter invocation matters. - val content = "#!/opt/whatever/launcher --flag\nexec java bsh.Interpreter \"\$0\"\nprint(1);" + val content = $$"#!/opt/whatever/launcher --flag\nexec java bsh.Interpreter \"$0\"\nprint(1);" assertEquals(BshFileType, detect(content)) } diff --git a/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt b/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt index 573593c..852ba0c 100644 --- a/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt +++ b/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt @@ -101,7 +101,7 @@ class BshScriptRewriterTest { // Maven interpolates ${...} in plugin config before afterProjectsRead, so the value we see is // already expanded; the manifest still holds the raw script the IDE captured. It must match. val expanded = "version = \"1.0.0\";\ngroupId = \"com.example.bsh\";\nok" - val raw = "version = \"\${project.version}\";\ngroupId = \"\${project.groupId}\";\nok" + val raw = $$"version = \"${project.version}\";\ngroupId = \"${project.groupId}\";\nok" val condition = element("condition", expanded) val plugin = beanshellPlugin(config(condition)).apply { artifactId = "maven-enforcer-plugin" } @@ -116,7 +116,7 @@ class BshScriptRewriterTest { // The ${...} tolerance must not turn into a wildcard that swallows an unrelated script. val condition = element("condition", "somethingElse();") val plugin = beanshellPlugin(config(condition)).apply { artifactId = "maven-enforcer-plugin" } - val raw = "version = \"\${project.version}\";\nok" + val raw = $$"version = \"${project.version}\";\nok" val replaced = BshScriptRewriter().instrumentPlugin(plugin, listOf(sub("condition", raw, "INSTR")), callbackJar) From 563ed60f6f2ca7a7c451e57333269cff9b9f7070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:05:49 +0200 Subject: [PATCH 07/28] Remove dead code from the hook module and plugin CMD_*/EVT_* in BshHook were superseded by DebugChannel.Command.Kind and only still referenced from this file's own Javadoc; NativeChannel keeps its own live copies for its own encoding, so those stay. Also drops DapChannel's unused MODE_RUN (its siblings are live) and Json.getBoolean (no callers, unlike its getInt/getString/getList siblings). Fixed the Javadoc @links left dangling by the removed constants to point at DebugChannel.Command.Kind instead. In the plugin's Kotlin sources: BshJavaResolver.resolveMember had no callers (call sites use resolveClassPsi + member directly); BshTokenTypes.NUMBERS/ BRACES were never wired into the highlighter, unlike their COMMENTS/ STRING_LITERALS/WHITESPACES siblings; BshDebugInstrumentation.SOURCES_PROPERTY was never read by name, only duplicated as a literal at its one call site; BshLineBreakpointType's PsiFile import was unused. Verified against the actual bytecode-injection/reflection call sites (ASM transformer, Sisu DI, BeanShell source-rewriting, plugin.xml extensions, JUnit) before deleting anything -- an exported whole-project "Inspect Code" run flags many things here as unused that are simply invisible to static analysis, not actually dead. --- .../main/java/cz/loplex/bsh/hook/BshHook.java | 68 +++---------------- .../java/cz/loplex/bsh/hook/DapChannel.java | 1 - .../main/java/cz/loplex/bsh/hook/Json.java | 6 -- .../bsh/debug/BshDebugInstrumentation.kt | 7 -- .../bsh/debug/BshLineBreakpointType.kt | 1 - .../loplex/intellij/bsh/psi/BshTokenTypes.kt | 7 -- .../intellij/bsh/reference/BshJavaResolver.kt | 6 -- 7 files changed, 9 insertions(+), 87 deletions(-) diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java index 451a941..2481852 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java @@ -1,15 +1,12 @@ package cz.loplex.bsh.hook; -import java.io.BufferedOutputStream; import java.io.BufferedReader; -import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.net.Socket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; @@ -35,9 +32,10 @@ * reflection throughout. Configuration arrives through system properties for the same reason. * *

The wire format is protocol 3, specified in {@code docs/PROTOCOL.md}. Per reported statement - * the hook writes {@link #EVT_STOPPED} with the call stack and then blocks that thread, - * answering whatever the IDE asks about its suspended frames — scopes, variables, an expression to - * evaluate, a value to change — until it is resumed. Other script threads keep running and may + * the hook writes via {@link DebugChannel#sendStopped} with the call stack and then blocks + * that thread, answering whatever the IDE asks about its suspended frames — scopes, + * variables, an expression to evaluate, a value to change — until it is resumed. Other script + * threads keep running and may * report alongside; see {@link ThreadState} and {@link #readerLoop} for how that works, and * {@link #report} for why only the reporting thread suspends. Failure handling is described on * {@link #onEval}. @@ -172,54 +170,6 @@ public final class BshHook { private static final String SWITCH_STATEMENT = "BSHSwitchStatement"; private static final String BLOCK = "BSHBlock"; - /* - * Commands the IDE may send on the return channel. RESUME releases a reported statement; any - * number of the others may precede it. - * - * Until the IDE sends SET_BREAKPOINTS at least once every statement is reported, because an IDE - * that configures nothing must not go blind. Once it does, the agent falls silent while running - * and speaks up only at a breakpoint, which removes the round-trip per statement that made a - * plain loop crawl. - */ - private static final int CMD_RESUME = 0x01; - private static final int CMD_SET_BREAKPOINTS = 0x02; - private static final int CMD_SET_RUN_MODE = 0x03; - - /** - * Turns "report everything, on every thread" on and off, so the IDE can round up the other - * threads when a breakpoint says Suspend: All. Global rather than per thread — that is what it - * means. See {@link #catchAll}. - */ - private static final int CMD_SET_CATCH_ALL = 0x08; - - /** - * Requests the IDE may issue while a statement is suspended, each answered with the matching - * {@code EVT_*} reply before the loop goes back to waiting. - * - *

They are served on the interpreter thread, from inside the same command loop that waits - * for {@link #CMD_RESUME}. That is not a shortcut: the thread is parked there anyway, it is - * the thread that owns the BeanShell state being inspected, and answering anywhere else would - * need a lock BeanShell does not offer. - */ - private static final int CMD_SCOPES = 0x04; - private static final int CMD_VARIABLES = 0x05; - private static final int CMD_EVALUATE = 0x06; - private static final int CMD_SET_VARIABLE = 0x07; - - /* - * The agent-to-IDE direction is opcode-tagged as of protocol 2. It used to be a bare stream of - * statement reports, which left no room for a reply to travel back the other way. - * - * There is no negotiation and no fallback to the old shape, because there is nothing to - * negotiate with: the agent jar ships inside the plugin, so both ends are always the same - * build. The tools in plugin/tools speak this format too. - */ - private static final int EVT_STOPPED = 0x10; - private static final int EVT_SCOPES = 0x11; - private static final int EVT_VARIABLES = 0x12; - private static final int EVT_EVALUATED = 0x13; - private static final int EVT_VARIABLE_SET = 0x14; - /** * Handle 0 is never issued, so the IDE can use it to mean "this value has no children" without * a separate flag on every variable. @@ -352,8 +302,6 @@ private static final class ThreadState { */ private static final long CONFIGURATION_TIMEOUT_MS = 30_000L; - private static final Object WRITE_LOCK = new Object(); - /** Guards {@link #connect} and the reader-thread start, which must happen exactly once. */ private static final Object CONNECT_LOCK = new Object(); @@ -1089,7 +1037,8 @@ private static String nullToEmpty(String value) { } /** - * Answers {@link #CMD_SCOPES}: the scopes of one frame, each a handle the IDE can expand. + * Answers {@link DebugChannel.Command.Kind#SCOPES}: the scopes of one frame, each a handle the + * IDE can expand. * *

Two scopes, and the second is the point of having the level at all: Global is the * interpreter's own namespace, which is where a script's top-level state lives once execution has @@ -1185,7 +1134,7 @@ static Outcome failed(String reason) { } /** - * Answers {@link #CMD_EVALUATE}: runs an expression in one frame's scope. + * Answers {@link DebugChannel.Command.Kind#EVALUATE}: runs an expression in one frame's scope. * *

The interpreter does the evaluating, so a watch expression sees exactly what the script * sees at that point — its variables, its methods, its imports — rather than a reimplementation @@ -1215,7 +1164,8 @@ private static Outcome evaluate(ThreadState state, int frameId, String expressio } /** - * Answers {@link #CMD_SET_VARIABLE}: evaluates an expression and stores it into {@code handle}. + * Answers {@link DebugChannel.Command.Kind#SET_VARIABLE}: evaluates an expression and stores it + * into {@code handle}. * *

A variable in scope is assigned by evaluating the assignment itself, so BeanShell applies * its own rules rather than this code guessing at them: a typed variable refuses an diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java index 701a05c..15cb9d8 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java @@ -443,7 +443,6 @@ private Command handleStep(int seq, String command, Object args) throws IOExcept private volatile int pendingStepThread; /** Run modes as the hook understands them; DAP's step requests are mapped onto these. */ - static final int MODE_RUN = 0; static final int MODE_OVER = 1; static final int MODE_INTO = 2; static final int MODE_OUT = 3; diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java index f19ebd5..6cf4053 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java @@ -161,12 +161,6 @@ static String getString(Object object, String key, String fallback) { return value instanceof String ? (String) value : fallback; } - /** A member as a boolean, or [fallback] when absent or not a boolean. */ - static boolean getBoolean(Object object, String key, boolean fallback) { - Object value = get(object, key); - return value instanceof Boolean ? (Boolean) value : fallback; - } - /** A member as a list, or an empty list when absent or not an array. */ @SuppressWarnings("unchecked") static List getList(Object object, String key) { diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt index 288da91..b975d35 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumentation.kt @@ -108,13 +108,6 @@ object BshDebugAgentJar { /** Override, mainly for development and for the command-line tools. */ const val PATH_PROPERTY: String = "bsh.debug.agent.jar" - /** - * Restricts the agent to the named sources, by file-name suffix (`cz.loplex.bsh.hook.BshHook`'s - * `bsh.debug.sources`). Repeated here as a literal because the agent ships as a plugin - * *resource*, not a dependency — its classes are deliberately not on the IDE's classpath. - */ - const val SOURCES_PROPERTY: String = "bsh.debug.sources" - /** * Restricts the agent to sources whose name starts with one of the prefixes in the named file * (`bsh.debug.sources.file`). What an inline script needs: handed a string, BeanShell names the diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt index c19bf1d..a2a0890 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt @@ -3,7 +3,6 @@ package cz.loplex.intellij.bsh.debug import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile import com.intellij.psi.PsiLanguageInjectionHost import com.intellij.psi.PsiManager import com.intellij.psi.util.PsiTreeUtil diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/psi/BshTokenTypes.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/psi/BshTokenTypes.kt index 64db759..7aebb43 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/psi/BshTokenTypes.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/psi/BshTokenTypes.kt @@ -1,6 +1,5 @@ package cz.loplex.intellij.bsh.psi -import com.intellij.psi.tree.IElementType import com.intellij.psi.tree.TokenSet /** @@ -58,12 +57,6 @@ object BshTokenTypes { @JvmField val STRING_LITERALS: TokenSet = TokenSet.create(STRING_LITERAL, CHARACTER_LITERAL) - @JvmField - val NUMBERS: TokenSet = TokenSet.create(INTEGER_LITERAL, FLOAT_LITERAL) - - @JvmField - val BRACES: TokenSet = TokenSet.create(LBRACE, RBRACE) - @JvmField val WHITESPACES: TokenSet = TokenSet.create(com.intellij.psi.TokenType.WHITE_SPACE) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt index a626f25..e078495 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshJavaResolver.kt @@ -44,12 +44,6 @@ object BshJavaResolver { return null } - /** Resolves a method or field named [memberName] on the Java class [typeName]. */ - fun resolveMember(context: PsiElement, typeName: String, memberName: String): PsiElement? { - val psiClass = resolveClass(context, typeName) as? PsiClass ?: return null - return member(psiClass, memberName) - } - fun resolveClassPsi(context: PsiElement, name: String): PsiClass? = resolveClass(context, name) as? PsiClass From e53f6c52357e351ba7964a21eb0c0fab13a19f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:06:22 +0200 Subject: [PATCH 08/28] Fix real findings flagged by inspections across the hook module and plugin BshHook.java: channel is assigned exactly once (across mutually exclusive branches of the same static initializer) and callStackTop is read only once, right where it's computed, so both lose their unnecessary staying-power: channel becomes final, callStackTop becomes a local instead of a class field. Suppressed two inspections whose suggestion would make the code worse: the mailbox queues are unbounded LinkedBlockingQueues, so offer()'s boolean result has no failure case worth checking; waitForConfiguration()'s Thread.sleep(20) is a deliberately bounded poll against a 30s deadline, not an unbounded spin. BshAmbiguousName extends BshNamedElement, so isReadWriteAccessible()'s second disjunct could never independently contribute anything -- simplified to the single check. Added clarifying parentheses around the offset+1 fallback in BshLineBreakpointType, per the operator-precedence inspection's suggestion. BshDebugRunner.kt: agentJar's null-check already flows through the local useAgent val, making the later !! redundant. BshMavenRunConfiguration.kt: MavenRunnerParameters.runnerParameters is @NotNull, making the safe call redundant too -- kept the workingDirPath ?: return fallback deliberately (suppressed, with a comment) since that nullability guarantee is upstream's to keep, not this code's. BshUnusedVariableInspection.kt: the preceding is/!is guard already lets Kotlin smart-cast to the common BshNamedElement supertype. BshParameterInfoHandler.kt: removed two ParameterInfoHandler overrides marked deprecated for removal upstream -- couldShowInLookup() returned true but getParametersForLookup() always returned null, so they were already a no-op; the interface has default implementations. BshMavenInjector used !isBlank() where isNotBlank() says the same thing more directly; BshScriptRewriterTest called setConfiguration() instead of the Kotlin property syntax the Maven model's Java bean already supports. --- agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java | 8 +++++--- .../intellij/bsh/completion/BshParameterInfoHandler.kt | 6 ------ .../kotlin/cz/loplex/intellij/bsh/debug/BshDebugRunner.kt | 2 +- .../cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt | 2 +- .../intellij/bsh/debug/maven/BshMavenRunConfiguration.kt | 5 ++++- .../cz/loplex/intellij/bsh/injection/BshMavenInjector.kt | 2 +- .../bsh/inspection/BshUnusedVariableInspection.kt | 2 +- .../intellij/bsh/reference/BshReadWriteAccessDetector.kt | 2 +- .../loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt | 2 +- 9 files changed, 15 insertions(+), 16 deletions(-) diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java index 2481852..c8b1c24 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java @@ -317,7 +317,7 @@ private static final class ThreadState { * channel decides how to encode it. That is what makes a second protocol a matter of one more * implementation rather than a second debugger. */ - private static DebugChannel channel; + private static final DebugChannel channel; // Reflection handles, resolved once. Every BSH* node inherits these from the // package-private bsh.SimpleNode, so a single Method works for all of them. @@ -328,7 +328,6 @@ private static final class ThreadState { private static Method nodeGetChild; private static Field whileIsDoStatement; private static Method callStackDepth; - private static Method callStackTop; private static Method nameSpaceGetVariableNames; private static Method nameSpaceGetVariable; private static Method nameSpaceGetParent; @@ -635,6 +634,7 @@ private static void drainMailbox(ThreadState state) { * {@link #applyCommand}. That was never a shortcut: only that thread can safely touch its own * BeanShell state, and answering from here would need a lock BeanShell does not offer. */ + @SuppressWarnings("ResultOfMethodCallIgnored") // mailbox is an unbounded queue; offer() cannot fail private static void readerLoop() { try { while (true) { @@ -754,6 +754,7 @@ private static void awaitResume(ThreadState state) throws IOException { } /** One place for "the IDE went away", which must never abort the host program. */ + @SuppressWarnings("ResultOfMethodCallIgnored") // mailbox is an unbounded queue; offer() cannot fail private static void sessionLost(IOException ex) { if (!disabled) { System.err.println("[bsh-agent] debug session disconnected; continuing without debugging (" @@ -937,6 +938,7 @@ private static boolean ensureConnected() { * program. Timing out means the script runs on unfiltered, which is the same outcome as an IDE that * never sends a breakpoint set. */ + @SuppressWarnings("BusyWait") // bounded 20ms poll against a 30s deadline, not an unbounded spin private static boolean waitForConfiguration() { if (!(channel instanceof DapChannel)) { return true; @@ -1585,7 +1587,7 @@ private static boolean resolveReflection(Object node, Object callstack) { Class callStackClass = callstack.getClass(); callStackDepth = accessible(callStackClass.getMethod("depth")); - callStackTop = accessible(callStackClass.getMethod("top")); + Method callStackTop = accessible(callStackClass.getMethod("top")); callStackToArray = accessible(callStackClass.getMethod("toArray")); nameSpaceClass = callStackTop.invoke(callstack).getClass(); diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/completion/BshParameterInfoHandler.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/completion/BshParameterInfoHandler.kt index 27c4e11..f838c9e 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/completion/BshParameterInfoHandler.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/completion/BshParameterInfoHandler.kt @@ -1,8 +1,6 @@ package cz.loplex.intellij.bsh.completion -import com.intellij.codeInsight.lookup.LookupElement import com.intellij.lang.parameterInfo.CreateParameterInfoContext -import com.intellij.lang.parameterInfo.ParameterInfoContext import com.intellij.lang.parameterInfo.ParameterInfoHandler import com.intellij.lang.parameterInfo.ParameterInfoUIContext import com.intellij.lang.parameterInfo.UpdateParameterInfoContext @@ -19,10 +17,6 @@ import cz.loplex.intellij.bsh.psi.BshElementTypes as E */ class BshParameterInfoHandler : ParameterInfoHandler { - override fun couldShowInLookup(): Boolean = true - - override fun getParametersForLookup(item: LookupElement?, context: ParameterInfoContext?): Array? = null - override fun findElementForParameterInfo(context: CreateParameterInfoContext): PsiElement? { val arguments = argumentsAt(context.file.findElementAt(context.offset)) ?: return null val method = resolveCallTarget(arguments) ?: return null diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugRunner.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugRunner.kt index a2527f8..bd9bb82 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugRunner.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugRunner.kt @@ -101,7 +101,7 @@ class BshDebugRunner : GenericProgramRunner() { ) } if (useAgent) { - commandLine.withParameters("-javaagent:${agentJar!!.absolutePath}") + commandLine.withParameters("-javaagent:${agentJar.absolutePath}") // Instrumenting the interpreter reaches strictly more code than rewriting one script // does, so without this the session would also stop inside BeanShell's own commands -- // print and friends are .bsh files on the classpath. diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt index a2a0890..fb520e4 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshLineBreakpointType.kt @@ -36,7 +36,7 @@ class BshLineBreakpointType : val element = psiFile.findElementAt(offset) val host = element?.let { PsiTreeUtil.getParentOfType(it, PsiLanguageInjectionHost::class.java, false) } if (host != null && isBeanShellInjected(manager, host)) return true - offset = (element?.textRange?.endOffset ?: offset + 1).coerceAtLeast(offset + 1) + offset = (element?.textRange?.endOffset ?: (offset + 1)).coerceAtLeast(offset + 1) } return false } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt index 201f402..95a63d6 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/maven/BshMavenRunConfiguration.kt @@ -90,7 +90,10 @@ class BshMavenRunConfiguration( /** Runs on the clone: prepares instrumentation, opens the socket and injects the `-D` contract. */ private fun setUpBeanShellDebug(environment: ExecutionEnvironment, mode: BshInstrumentationMode) { try { - val workDirPath = runnerParameters?.workingDirPath ?: return + // Kotlin trusts MavenRunnerParameters' @NotNull annotation on workingDirPath, but that's + // an upstream guarantee, not one this code controls -- keep the fallback in case it's ever wrong. + @Suppress("USELESS_ELVIS") + val workDirPath = runnerParameters.workingDirPath ?: return val pomFile = LocalFileSystem.getInstance().findFileByIoFile(File(workDirPath, "pom.xml")) ?: return val prepared = ReadAction.compute, RuntimeException> { BshMavenDebugSupport.prepare(project, pomFile) diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt index d4a2240..c676900 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt @@ -65,7 +65,7 @@ class BshMavenInjector : MultiHostInjector { while (sibling != null) { when (sibling) { is PsiWhiteSpace -> {} - is XmlText -> if (!sibling.text.isBlank()) return false + is XmlText -> if (sibling.text.isNotBlank()) return false is XmlComment -> return LANGUAGE_COMMENT.containsMatchIn(sibling.text) else -> return false } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/inspection/BshUnusedVariableInspection.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/inspection/BshUnusedVariableInspection.kt index 8adc73f..ffeca75 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/inspection/BshUnusedVariableInspection.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/inspection/BshUnusedVariableInspection.kt @@ -22,7 +22,7 @@ class BshUnusedVariableInspection : LocalInspectionTool() { object : PsiElementVisitor() { override fun visitElement(element: PsiElement) { if (element !is BshVariableDeclarator && element !is BshFormalParameter) return - val named = element as BshNamedElement + val named: BshNamedElement = element val identifier = named.nameIdentifier ?: return val file = element.containingFile ?: return diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshReadWriteAccessDetector.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshReadWriteAccessDetector.kt index 257f511..1e648f8 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshReadWriteAccessDetector.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/reference/BshReadWriteAccessDetector.kt @@ -13,7 +13,7 @@ import cz.loplex.intellij.bsh.psi.BshNamedElement class BshReadWriteAccessDetector : ReadWriteAccessDetector() { override fun isReadWriteAccessible(element: PsiElement): Boolean = - element is BshNamedElement || element is BshAmbiguousName + element is BshNamedElement override fun isDeclarationWriteAccess(element: PsiElement): Boolean = element is BshAmbiguousName && BshScopes.isSimpleAssignmentTarget(element) diff --git a/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt b/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt index 852ba0c..fd5dcea 100644 --- a/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt +++ b/plugin/src/test/kotlin/cz/loplex/intellij/bsh/mavenext/BshScriptRewriterTest.kt @@ -51,7 +51,7 @@ class BshScriptRewriterTest { @Test fun replacesExecutionLevelScript() { val plugin = beanshellPlugin(null) - plugin.addExecution(PluginExecution().apply { id = "run"; setConfiguration(config(element("script", "old;"))) }) + plugin.addExecution(PluginExecution().apply { id = "run"; configuration = config(element("script", "old;")) }) val replaced = BshScriptRewriter().instrumentPlugin(plugin, listOf(sub("script", "old;", "NEW")), callbackJar) From a2757868cbce3d5b192d8cb272b83e278f5a07c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:06:51 +0200 Subject: [PATCH 09/28] Fix UI-facing strings and add missing description resources Surround With menu entries and parser error messages need title/sentence capitalization per platform convention; the file-chooser dialog title and the Maven action text used the internal "bsh" short name where every sibling dialog/action in the same files uses the full "BeanShell" name. The three bundled local inspections had no inspectionDescriptions/*.html, the introduce-variable intention was missing its before/after example templates (description.html already existed), and the postfix template provider had neither a description nor examples. These back the tooltip JetBrains' own inspections/intentions/postfix templates show in Settings. --- .../intellij/bsh/editor/BshSurrounders.kt | 6 ++-- .../loplex/intellij/bsh/parser/BshParser.kt | 32 +++++++++---------- .../intellij/bsh/run/BshSettingsEditor.kt | 2 +- .../src/main/resources/META-INF/bsh-maven.xml | 4 +-- .../BshUnreachableCode.html | 8 +++++ .../BshUnresolvedMethod.html | 9 ++++++ .../BshUnusedVariable.html | 8 +++++ .../after.bsh.template | 1 + .../before.bsh.template | 1 + .../after.bsh.template | 1 + .../before.bsh.template | 1 + .../BshStringPostfixTemplate/description.html | 9 ++++++ 12 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 plugin/src/main/resources/inspectionDescriptions/BshUnreachableCode.html create mode 100644 plugin/src/main/resources/inspectionDescriptions/BshUnresolvedMethod.html create mode 100644 plugin/src/main/resources/inspectionDescriptions/BshUnusedVariable.html create mode 100644 plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/after.bsh.template create mode 100644 plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/before.bsh.template create mode 100644 plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/after.bsh.template create mode 100644 plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/before.bsh.template create mode 100644 plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/description.html diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshSurrounders.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshSurrounders.kt index 0d1fc5a..459f7b9 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshSurrounders.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/editor/BshSurrounders.kt @@ -48,17 +48,17 @@ abstract class BshSurrounderBase : Surrounder { } class BshIfSurrounder : BshSurrounderBase() { - override fun getTemplateDescription(): String = "if" + override fun getTemplateDescription(): String = "If" override fun wrap(body: String): Pair = "if () {\n$body\n}" to "if (".length } class BshWhileSurrounder : BshSurrounderBase() { - override fun getTemplateDescription(): String = "while" + override fun getTemplateDescription(): String = "While" override fun wrap(body: String): Pair = "while () {\n$body\n}" to "while (".length } class BshTrySurrounder : BshSurrounderBase() { - override fun getTemplateDescription(): String = "try / catch" + override fun getTemplateDescription(): String = "Try / Catch" override fun wrap(body: String): Pair { val text = "try {\n$body\n} catch (e) {\n}" return text to text.length - 1 // caret inside the catch block diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt index 0992c38..3a5dd28 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt @@ -247,7 +247,7 @@ class BshParser : PsiParser { return } m.drop() - b.error("parameter expected") + b.error("Parameter expected") } private fun parseVariableDeclarator() { @@ -288,7 +288,7 @@ class BshParser : PsiParser { consume() if (atText(":")) { consume() - if (!parseStatement()) b.error("statement expected") + if (!parseStatement()) b.error("Statement expected") m.done(E.LABELED_STATEMENT) return true } @@ -346,10 +346,10 @@ class BshParser : PsiParser { val m = b.mark() consume() // if expect(T.LPAREN, "("); parseExpressionOrError(); expect(T.RPAREN, ")") - if (!parseStatement()) b.error("statement expected") + if (!parseStatement()) b.error("Statement expected") if (isKeywordText("else")) { consume() - if (!parseStatement()) b.error("statement expected") + if (!parseStatement()) b.error("Statement expected") } m.done(E.IF_STATEMENT) return true @@ -359,7 +359,7 @@ class BshParser : PsiParser { val m = b.mark() consume() // while expect(T.LPAREN, "("); parseExpressionOrError(); expect(T.RPAREN, ")") - if (!parseStatement()) b.error("statement expected") + if (!parseStatement()) b.error("Statement expected") m.done(E.WHILE_STATEMENT) return true } @@ -367,7 +367,7 @@ class BshParser : PsiParser { private fun parseDo(): Boolean { val m = b.mark() consume() // do - if (!parseStatement()) b.error("statement expected") + if (!parseStatement()) b.error("Statement expected") expectText("while") expect(T.LPAREN, "("); parseExpressionOrError(); expect(T.RPAREN, ")") expect(T.SEMICOLON, ";") @@ -396,7 +396,7 @@ class BshParser : PsiParser { expectText(":") parseExpressionOrError() expect(T.RPAREN, ")") - if (!parseStatement()) b.error("statement expected") + if (!parseStatement()) b.error("Statement expected") m2.done(E.ENHANCED_FOR_STATEMENT) return true } @@ -519,7 +519,7 @@ class BshParser : PsiParser { // --------------------------------------------------------------------- private fun parseExpressionOrError() { - if (!parseExpression()) b.error("expression expected") + if (!parseExpression()) b.error("Expression expected") } private fun parseExpression(): Boolean { @@ -545,7 +545,7 @@ class BshParser : PsiParser { consume() parseExpressionOrError() expectText(":") - if (!parseConditionalExpression()) b.error("expression expected") + if (!parseConditionalExpression()) b.error("Expression expected") m.done(E.TERNARY_EXPRESSION) } else { m.drop() @@ -565,7 +565,7 @@ class BshParser : PsiParser { if (!parseRelational()) { m.drop(); return false } if (isKeywordText("instanceof")) { consume() - if (!parseType()) b.error("type expected") + if (!parseType()) b.error("Type expected") m.done(E.BINARY_EXPRESSION) } else { m.drop() @@ -583,7 +583,7 @@ class BshParser : PsiParser { if (!next()) { m.drop(); return false } while (at(T.OPERATOR) && b.tokenText in ops) { consume() - if (!next()) b.error("expression expected") + if (!next()) b.error("Expression expected") m.done(E.BINARY_EXPRESSION) m = m.precede() } @@ -594,12 +594,12 @@ class BshParser : PsiParser { private fun parseUnary(): Boolean { if (atText("+") || atText("-")) { val m = b.mark(); consume() - if (!parseUnary()) b.error("expression expected") + if (!parseUnary()) b.error("Expression expected") m.done(E.UNARY_EXPRESSION); return true } if (atText("++") || atText("--")) { val m = b.mark(); consume() - if (!parsePrimaryExpression()) b.error("expression expected") + if (!parsePrimaryExpression()) b.error("Expression expected") m.done(E.UNARY_EXPRESSION); return true } return parseUnaryNotPlusMinus() @@ -608,7 +608,7 @@ class BshParser : PsiParser { private fun parseUnaryNotPlusMinus(): Boolean { if (atText("~") || atText("!")) { val m = b.mark(); consume() - if (!parseUnary()) b.error("expression expected") + if (!parseUnary()) b.error("Expression expected") m.done(E.UNARY_EXPRESSION); return true } if (tryCast()) return true @@ -698,7 +698,7 @@ class BshParser : PsiParser { return true } consume() // . - if (atText("class")) consume() else b.error("identifier or 'class' expected") + if (atText("class")) consume() else b.error("Identifier or 'class' expected") return true } if (at(T.LBRACKET)) { @@ -733,7 +733,7 @@ class BshParser : PsiParser { else -> b.error("'(' or '[' expected") } } else { - b.error("type expected") + b.error("Type expected") } m.done(E.ALLOCATION_EXPRESSION) return true diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt index 864d1ec..648b545 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/run/BshSettingsEditor.kt @@ -37,7 +37,7 @@ class BshSettingsEditor : SettingsEditor() { interpreterClasspath.addBrowseFolderListener( TextBrowseFolderListener( FileChooserDescriptor(true, true, true, true, false, false) - .withTitle("Select bsh Jar or Classes Directory") + .withTitle("Select BeanShell Jar or Classes Directory") ) ) jrePath.addBrowseFolderListener( diff --git a/plugin/src/main/resources/META-INF/bsh-maven.xml b/plugin/src/main/resources/META-INF/bsh-maven.xml index 9f61c1f..3200fcf 100644 --- a/plugin/src/main/resources/META-INF/bsh-maven.xml +++ b/plugin/src/main/resources/META-INF/bsh-maven.xml @@ -14,12 +14,12 @@ topic="com.intellij.execution.ExecutionListener"/> - diff --git a/plugin/src/main/resources/inspectionDescriptions/BshUnreachableCode.html b/plugin/src/main/resources/inspectionDescriptions/BshUnreachableCode.html new file mode 100644 index 0000000..bf840cd --- /dev/null +++ b/plugin/src/main/resources/inspectionDescriptions/BshUnreachableCode.html @@ -0,0 +1,8 @@ + + +Reports a statement that follows an unconditional control-flow exit +(return or throw) within the same block, and can +therefore never run. + + + diff --git a/plugin/src/main/resources/inspectionDescriptions/BshUnresolvedMethod.html b/plugin/src/main/resources/inspectionDescriptions/BshUnresolvedMethod.html new file mode 100644 index 0000000..a3a0ec7 --- /dev/null +++ b/plugin/src/main/resources/inspectionDescriptions/BshUnresolvedMethod.html @@ -0,0 +1,9 @@ + + +Reports a method call whose target cannot be resolved anywhere in the project. + +

Disabled by default: BeanShell scripts routinely call Java library methods and +built-in commands (such as print) that this plugin does not model, so +enabling this inspection is opt-in and may report false positives for those calls.

+ + diff --git a/plugin/src/main/resources/inspectionDescriptions/BshUnusedVariable.html b/plugin/src/main/resources/inspectionDescriptions/BshUnusedVariable.html new file mode 100644 index 0000000..d570484 --- /dev/null +++ b/plugin/src/main/resources/inspectionDescriptions/BshUnusedVariable.html @@ -0,0 +1,8 @@ + + +Reports a typed variable or parameter that is declared but never read within the file. + +

Untyped variables are intentionally not flagged: every occurrence of an untyped +name is itself an assignment target, so "never used" is not well defined for them.

+ + diff --git a/plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/after.bsh.template b/plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/after.bsh.template new file mode 100644 index 0000000..40843e4 --- /dev/null +++ b/plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/after.bsh.template @@ -0,0 +1 @@ +x = compute(); diff --git a/plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/before.bsh.template b/plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/before.bsh.template new file mode 100644 index 0000000..66acca5 --- /dev/null +++ b/plugin/src/main/resources/intentionDescriptions/BshIntroduceVariableIntention/before.bsh.template @@ -0,0 +1 @@ +compute(); diff --git a/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/after.bsh.template b/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/after.bsh.template new file mode 100644 index 0000000..c72f2bc --- /dev/null +++ b/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/after.bsh.template @@ -0,0 +1 @@ +print(compute()); diff --git a/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/before.bsh.template b/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/before.bsh.template new file mode 100644 index 0000000..1665030 --- /dev/null +++ b/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/before.bsh.template @@ -0,0 +1 @@ +compute().sout diff --git a/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/description.html b/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/description.html new file mode 100644 index 0000000..12cbf21 --- /dev/null +++ b/plugin/src/main/resources/postfixTemplates/BshStringPostfixTemplate/description.html @@ -0,0 +1,9 @@ + + +Wraps the expression before the postfix dot in a surrounding construct. + +

expr.sout becomes print(expr);, +expr.if becomes if (expr) {...}, and +expr.while becomes while (expr) {...}.

+ + From 1434d0c1884cf58e4d64d3d616b4ff1a355898dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:07:19 +0200 Subject: [PATCH 10/28] Suppress warnings for design-intentional patterns across the plugin and VS Code extension Mark the fire-and-forget showErrorMessage calls in configurationProvider.ts as intentional: each call is immediately followed by a synchronous return, there is no button/action to react to, so there is nothing to await; void makes that explicit instead of leaving the returned promise looking abandoned. Also suppresses SameParameterValue, SameReturnValue, and JSUnusedGlobalSymbols where the inspections' suggestions don't apply. --- agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java | 1 + .../hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java | 2 +- agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java | 6 +++--- editors/vscode/src/configurationProvider.ts | 8 ++++---- editors/vscode/src/extension.ts | 2 ++ .../cz/loplex/intellij/bsh/injection/BshMavenInjector.kt | 1 + .../kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt | 2 ++ 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java index c8b1c24..553f9a1 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java @@ -396,6 +396,7 @@ private BshHook() { } /** A port property, or [fallback] when unset or unparseable. */ + @SuppressWarnings("SameParameterValue") private static int parsedPort(String property, int fallback) { String value = System.getProperty(property); if (value == null || value.trim().isEmpty()) { diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java index 15cb9d8..00d20cc 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java @@ -321,7 +321,7 @@ private Command handleInitialize(int seq, String command) throws IOException { private Command handleSetBreakpoints(int seq, String command, Object args) throws IOException { Object source = Json.get(args, "source"); String path = Json.getString(source, "path", Json.getString(source, "name", "")); - List requested = Json.getList(args, "breakpoints"); + List requested = Json.getList(args, "breakpoints"); int[] lines = new int[requested.size()]; List verified = new ArrayList<>(); diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java index 6cf4053..68b8f2b 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/Json.java @@ -162,10 +162,10 @@ static String getString(Object object, String key, String fallback) { } /** A member as a list, or an empty list when absent or not an array. */ - @SuppressWarnings("unchecked") - static List getList(Object object, String key) { + @SuppressWarnings("SameParameterValue") + static List getList(Object object, String key) { Object value = get(object, key); - return value instanceof List ? (List) value : new ArrayList<>(); + return value instanceof List ? (List) value : new ArrayList<>(); } private static final class Parser { diff --git a/editors/vscode/src/configurationProvider.ts b/editors/vscode/src/configurationProvider.ts index b1be2d1..c4c2f37 100644 --- a/editors/vscode/src/configurationProvider.ts +++ b/editors/vscode/src/configurationProvider.ts @@ -27,18 +27,18 @@ export class BshConfigurationProvider implements vscode.DebugConfigurationProvid if (config.request === 'launch') { if (!config.script) { - vscode.window.showErrorMessage('BeanShell launch configuration is missing "script".'); + void vscode.window.showErrorMessage('BeanShell launch configuration is missing "script".'); return undefined; } if (!config.agentJar) { - vscode.window.showErrorMessage( + void vscode.window.showErrorMessage( 'BeanShell launch configuration is missing "agentJar" (the bsh-debug-agent jar ' + 'built by ./gradlew :agent:instrument:shadowJar).' ); return undefined; } if (!config.classpath) { - vscode.window.showErrorMessage( + void vscode.window.showErrorMessage( 'BeanShell launch configuration is missing "classpath" (it must include the ' + 'BeanShell jar).' ); @@ -48,7 +48,7 @@ export class BshConfigurationProvider implements vscode.DebugConfigurationProvid config.sources = config.sources || path.basename(config.script); } else if (config.request === 'attach') { if (!config.port) { - vscode.window.showErrorMessage('BeanShell attach configuration is missing "port".'); + void vscode.window.showErrorMessage('BeanShell attach configuration is missing "port".'); return undefined; } config.host = config.host || '127.0.0.1'; diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index c280049..de32867 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -1,3 +1,5 @@ +// noinspection JSUnusedGlobalSymbols + import * as vscode from 'vscode'; import { BshConfigurationProvider } from './configurationProvider'; import { BshDebugAdapterDescriptorFactory } from './descriptorFactory'; diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt index c676900..d6bf046 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/injection/BshMavenInjector.kt @@ -92,6 +92,7 @@ class BshMavenInjector : MultiHostInjector { return false } + @Suppress("SameParameterValue") private fun childValue(tag: XmlTag, name: String): String? = tag.findFirstSubTag(name)?.value?.trimmedText } diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt index 3a5dd28..9b0ead7 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/parser/BshParser.kt @@ -19,6 +19,7 @@ import cz.loplex.intellij.bsh.psi.BshTokenTypes as T * ordered-choice semantics of the original grammar while remaining tolerant of * incomplete input in the editor. */ +@Suppress("SameReturnValue") class BshParser : PsiParser { private lateinit var b: PsiBuilder @@ -46,6 +47,7 @@ class BshParser : PsiParser { /** Matches by token text; works for keywords, operators and separators. */ private fun atText(text: String): Boolean = b.tokenText == text + @Suppress("SameParameterValue") private fun lookAhead(n: Int): IElementType? = b.lookAhead(n) private fun consume() = b.advanceLexer() From 77fbf4943f7e09ad031b0752885b1207575c786d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:08:04 +0200 Subject: [PATCH 11/28] Fix warnings in check-runner and dev tool scripts 'index' was never read inside the DAP client check script's loop body. ShellCheck (SC2164): guard cd against failure in the check-runner shell scripts -- if dirname's output somehow doesn't exist, the script would silently keep running from the wrong directory instead of stopping. run-all.sh: expand the glob into an array first so ShellCheck doesn't flag the for-loop's unquoted expansion. check-instrumentation.py: report()'s loop variable shadowed the outer for-loop's 'number'. mock-ide.py: merged the "frames" key into the dict literal it was appended to right after (same read order preserved), dropped redundant tuple parens on a bare return, and suppressed two PyCharm false positives -- a "Format spec not supported for Queue" on an f-string whose operands are plain ints, and "Session doesn't have attribute pack" on resume(s, ...) call sites, when resume() only ever calls s.send() and struct.pack() (on the struct module, never on s). docs/BEANSHELL-DEFECTS.md: mark the repro's code fence as jshelllanguage so it doesn't get flagged as unparseable Java, and annotate it with @SuppressWarnings("EmptyFinallyBlock") since the empty finally is the point of the repro. --- agent/checks/dap-client.py | 2 +- agent/checks/run-all.sh | 5 +++-- docs/BEANSHELL-DEFECTS.md | 3 ++- editors/neovim/tests/run-tests.sh | 2 +- plugin/tools/check-instrumentation.py | 4 ++-- plugin/tools/mock-ide.py | 12 ++++++++---- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/agent/checks/dap-client.py b/agent/checks/dap-client.py index d847479..113736c 100755 --- a/agent/checks/dap-client.py +++ b/agent/checks/dap-client.py @@ -171,7 +171,7 @@ def main(): conn.request("configurationDone") print("[dap] configurationDone", flush=True) - for index in range(args.stops): + for _ in range(args.stops): stopped = conn.wait_event("stopped", timeout=30) if stopped is None: print("[dap] no further stops", flush=True) diff --git a/agent/checks/run-all.sh b/agent/checks/run-all.sh index 807a126..c515006 100755 --- a/agent/checks/run-all.sh +++ b/agent/checks/run-all.sh @@ -7,10 +7,11 @@ # diagnosis. set -uo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" +cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 +files=( [0-9][0-9]-*.sh ) failed=() -for check in [0-9][0-9]-*.sh; do +for check in "${files[@]}"; do if ! bash "$check"; then failed+=("$check") fi diff --git a/docs/BEANSHELL-DEFECTS.md b/docs/BEANSHELL-DEFECTS.md index a0e77ec..d2d6dc4 100644 --- a/docs/BEANSHELL-DEFECTS.md +++ b/docs/BEANSHELL-DEFECTS.md @@ -40,7 +40,7 @@ at runtime and package-private access fails. Even an empty `finally`. The root cause is one line, `BSHTryStatement.java:173`: -```java +```jshelllanguage if (finallyBlock != null) ret = finallyBlock.eval(callstack, interpreter); ``` @@ -49,6 +49,7 @@ It overwrites `ret` unconditionally, discarding the `ReturnControl` produced by try/catch block. Minimal repro: ```java +@SuppressWarnings("EmptyFinallyBlock") f() { try { return "ok"; } finally { } } f(); // yields void ``` diff --git a/editors/neovim/tests/run-tests.sh b/editors/neovim/tests/run-tests.sh index ffe7afe..c1fa3ef 100755 --- a/editors/neovim/tests/run-tests.sh +++ b/editors/neovim/tests/run-tests.sh @@ -7,7 +7,7 @@ # Usage: ./editors/neovim/tests/run-tests.sh set -uo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" +cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 TESTS_DIR="$(pwd)" REPO_ROOT="$(cd ../../.. && pwd)" GRADLEW="$REPO_ROOT/gradlew" diff --git a/plugin/tools/check-instrumentation.py b/plugin/tools/check-instrumentation.py index edd25da..fb87ff4 100755 --- a/plugin/tools/check-instrumentation.py +++ b/plugin/tools/check-instrumentation.py @@ -170,8 +170,8 @@ def take(flag, default=None): def report(title, numbers): print(f"{title}: {len(numbers)}") - for number in numbers: - print(f" line {number:<4} {source[number - 1].strip()[:70]}") + for n in numbers: + print(f" line {n:<4} {source[n - 1].strip()[:70]}") print(f"{sample}: {len(wanted)} marked lines, target={target}") if missing: diff --git a/plugin/tools/mock-ide.py b/plugin/tools/mock-ide.py index a02df00..34bd99b 100755 --- a/plugin/tools/mock-ide.py +++ b/plugin/tools/mock-ide.py @@ -163,10 +163,10 @@ def _read_loop(self): "thread_name": rutf(self.f), "line": rint(self.f), "depth": rint(self.f), + "frames": [ + (rutf(self.f), rutf(self.f), rint(self.f)) for _ in range(rint(self.f)) + ], } - stop["frames"] = [ - (rutf(self.f), rutf(self.f), rint(self.f)) for _ in range(rint(self.f)) - ] self.stops.put(stop) elif event in REPLY_EVENTS: request_id = rint(self.f) @@ -200,7 +200,7 @@ def _read_reply(self, event): (rutf(self.f), rutf(self.f), rutf(self.f), rint(self.f)) for _ in range(rint(self.f)) ] # evaluate / set-variable share one shape: ok, then value/type/handle. - return (rbyte(self.f) != 0, rutf(self.f), rutf(self.f), rint(self.f)) + return rbyte(self.f) != 0, rutf(self.f), rutf(self.f), rint(self.f) def send(self, payload): with self._lock: @@ -219,6 +219,7 @@ def request(self, event, build): raise EOFError got, payload = answer if got != event: + # noinspection PyStringFormat sys.exit(f"[mock-ide] expected 0x{event:02x} for request {request_id}, got 0x{got:02x}") return payload @@ -359,6 +360,7 @@ def main(): print("[mock-ide] catch-all off", flush=True) for thread in held: print(f"[mock-ide] releasing held thread={thread}", flush=True) + # noinspection PyTypeChecker resume(s, thread) held = [] continue @@ -421,10 +423,12 @@ def main(): print("[mock-ide] catch-all off", flush=True) for thread in held: print(f"[mock-ide] releasing held thread={thread}", flush=True) + # noinspection PyTypeChecker resume(s, thread) held = [] continue + # noinspection PyTypeChecker resume(s, stop["thread"]) if s.error: From 8701703b608e19f82f62293411f18645d7544439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:08:34 +0200 Subject: [PATCH 12/28] Fix broken doc links and stale references across the codebase BshHook.java: {@link #connect} pointed at a method that lives on DebugChannel, not BshHook itself. Two Javadoc comments had gone dangling -- one describing a write-serialization lock that was refactored away (each channel implementation now has its own writeLock), the other a stale duplicate of the per-thread handles field's doc, both left behind by an earlier refactor with nothing left to attach to. DebugChannel.java: {@link Answer} referenced a type that was never added; the word was meant as plain prose. [lineFor] can't resolve from BshDebugInstrumenter's class-level KDoc comment -- it's a parameter name, only linkable from within the function that declares it. editors/{eclipse,neovim,vscode}/README.md linked bare sibling directories (e.g. ../vscode/); IntelliJ's markdown resolver doesn't apply GitHub's directory-implies-README convention, so point at the README.md files directly -- still renders identically on GitHub. docs/PROTOCOL.md's link to FUTURE_WORK.md used a stale anchor from before that section was renamed to "DAP as a second transport -- done". Also added a few missing commas before coordinating conjunctions joining independent clauses, in DebugHost.java and across the docs. --- .../main/java/cz/loplex/bsh/hook/BshHook.java | 22 +------------------ .../java/cz/loplex/bsh/hook/DebugChannel.java | 2 +- agent/samples/src/main/java/DebugHost.java | 2 +- docs/PROTOCOL.md | 4 ++-- editors/eclipse/README.md | 20 ++++++++--------- editors/neovim/README.md | 2 +- editors/vscode/README.md | 4 ++-- plugin/docs/DEBUGGING.md | 2 +- .../bsh/debug/BshDebugInstrumenter.kt | 2 +- 9 files changed, 20 insertions(+), 40 deletions(-) diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java index 553f9a1..3c610c0 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java @@ -267,18 +267,6 @@ private static final class ThreadState { */ private static final ThreadLocal REPORTING = new ThreadLocal<>(); - /** - * Serialises writes to the socket, and nothing else. - * - *

This is the whole of what used to be {@code LOCK}. Before threads, one lock covered - * connecting, writing, and being suspended — which is precisely why two threads could not be - * suspended at once: the first held it for the duration of its stop. Now a stop holds no lock at - * all; it parks on its own mailbox, and this guards only the moments when bytes are being put on - * the wire, so a second thread can report while the first is still suspended. - * - *

A message must be written under a single acquisition, or two threads' fields would - * interleave into an unparseable stream. - */ /** * Whether every thread should report its next statement, whatever the breakpoints say. * @@ -302,7 +290,7 @@ private static final class ThreadState { */ private static final long CONFIGURATION_TIMEOUT_MS = 30_000L; - /** Guards {@link #connect} and the reader-thread start, which must happen exactly once. */ + /** Guards {@link DebugChannel#connect} and the reader-thread start, which must happen exactly once. */ private static final Object CONNECT_LOCK = new Object(); private static final int port; @@ -342,14 +330,6 @@ private static final class ThreadState { private static Method primitiveGetType; private static boolean reflectionFailed; - /** - * Objects the IDE may ask to expand, valid only for the current stop. - * - *

Discarded on every resume, which is the whole reason handles are safe: the IDE can never - * hold a reference into a script that has moved on, so there is no stale-object problem to - * solve and no cleanup protocol to get wrong. This mirrors DAP, where a - * {@code variablesReference} is explicitly invalid once execution continues. - */ static { int parsed = -1; String portProperty = System.getProperty(PORT_PROPERTY); diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java index 1528aa9..8ed581d 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java @@ -9,7 +9,7 @@ *

This interface is where the two transports part company, and the split is deliberately placed so * that nothing interesting lives on the transport side. Deciding what counts as a statement, * walking the call stack, rendering values, handing out handles, evaluating in a frame's namespace — - * all of that produces {@link Answer}s and is written once. Only the last step, turning an answer into + * all of that produces answers and is written once. Only the last step, turning an answer into * bytes, exists twice: {@link NativeChannel} as the compact binary protocol IntelliJ speaks, and * {@link DapChannel} as JSON over Content-Length framing for anything else. * diff --git a/agent/samples/src/main/java/DebugHost.java b/agent/samples/src/main/java/DebugHost.java index 5ebcb91..6a955b8 100644 --- a/agent/samples/src/main/java/DebugHost.java +++ b/agent/samples/src/main/java/DebugHost.java @@ -66,7 +66,7 @@ private static void banner(String s) { } /** - * 1. Plain source() of a file. This is the common library pattern and it + * 1. Plain source() of a file. This is the common library pattern, and it * lands in Interpreter.eval(Reader, NameSpace, String) -- NOT in run(). */ private static void scenario1_sourceFile() throws Exception { diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index c50709e..b74605d 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -270,7 +270,7 @@ These are what an implementation can rely on, and what it must preserve. request as its own answer, which is why a timed-out channel had to be written off for good. With ids, a late reply simply finds no waiter and is dropped. -2. **By default only the thread that hit a breakpoint is suspended.** Others keep running, and +2. **By default, only the thread that hit a breakpoint is suspended.** Others keep running, and may report while it is parked. A breakpoint whose policy is Suspend: All rounds them up via [`SET_CATCH_ALL`](#0x08-set_catch_all) — approximately, and the approximation is inherent: a thread is only ever stopped where it calls the hook, so one inside Java code cannot be frozen. @@ -290,7 +290,7 @@ These are what an implementation can rely on, and what it must preserve. 6. **No version negotiation**, because there is nothing to negotiate with: the agent jar ships inside the plugin, so both ends are always the same build. An independently published agent - would change this — see [`FUTURE_WORK.md`](FUTURE_WORK.md#dap-as-the-transport). + would change this — see [`FUTURE_WORK.md`](FUTURE_WORK.md#dap-as-a-second-transport--done). ### What the rewriting agent does differently diff --git a/editors/eclipse/README.md b/editors/eclipse/README.md index 53b5004..568544e 100644 --- a/editors/eclipse/README.md +++ b/editors/eclipse/README.md @@ -6,8 +6,8 @@ Eclipse has no built-in DAP client of its own; the generic one is own [`docs/FUTURE_WORK.md`](../../docs/FUTURE_WORK.md) already names as the reason the native protocol and DAP stay separate transports. Same [debug agent](../../agent/README.md), same [DAP transport](../../docs/PROTOCOL.md#9-relationship-to-dap) as -[`../vscode/`](../vscode/) and [`../neovim/`](../neovim/) — this is packaging for a third -editor, not a third implementation. +[`../vscode/`](../vscode/README.md) and [`../neovim/`](../neovim/README.md) — this is packaging +for a third editor, not a third implementation. **Attach only.** LSP4E's generic launcher can either start a Debug Adapter Server itself or connect to one already running, but starting one means spawning something that speaks DAP — @@ -55,14 +55,14 @@ interrupt), no conditional/function/exception breakpoints, no step-back, no rest ## Manual verification runbook -Unlike [`../vscode/`](../vscode/#testing) and [`../neovim/`](../neovim/#testing), there is no -automated end-to-end test here. Both of those cover code this repository owns — the extension's -own JVM launch, `bsh-dap.lua`'s own launch — that a hand-rolled DAP client can't exercise. There -is no equivalent here: this package is a README, not a launcher, and LSP4E's generic Debug -Adapter launch configuration (configured entirely through its own UI dialog) is upstream code -this repository doesn't own. Automating it would mean standing up a second build toolchain -(Tycho, a p2 target platform, SWTBot) to re-verify that *LSP4E* speaks DAP correctly against this -agent — already proven, against the same agent, by +Unlike [`../vscode/`](../vscode/README.md#testing) and [`../neovim/`](../neovim/README.md#testing), +there is no automated end-to-end test here. Both of those cover code this repository owns — the +extension's own JVM launch, `bsh-dap.lua`'s own launch — that a hand-rolled DAP client can't +exercise. There is no equivalent here: this package is a README, not a launcher, and LSP4E's +generic Debug Adapter launch configuration (configured entirely through its own UI dialog) is +upstream code this repository doesn't own. Automating it would mean standing up a second build +toolchain (Tycho, a p2 target platform, SWTBot) to re-verify that *LSP4E* speaks DAP correctly +against this agent — already proven, against the same agent, by [`agent/checks/07-dap-transport.sh`](../../agent/checks/07-dap-transport.sh)'s `dap-client.py`. What's worth checking by hand — after touching the agent, the DAP transport, or this doc — is diff --git a/editors/neovim/README.md b/editors/neovim/README.md index cd38ef1..cfa8b46 100644 --- a/editors/neovim/README.md +++ b/editors/neovim/README.md @@ -3,7 +3,7 @@ Wires the [debug agent](../../agent/README.md)'s [DAP transport](../../docs/PROTOCOL.md#9-relationship-to-dap) into [nvim-dap](https://github.com/mfussenegger/nvim-dap). Same agent, same protocol as -[`../vscode/`](../vscode/) — this is packaging for a different editor, not a second +[`../vscode/`](../vscode/README.md) — this is packaging for a different editor, not a second implementation. ## Setup diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 8626ef0..e4466a4 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -97,5 +97,5 @@ a VS Code build under `.vscode-test/`. ## Alternatives -[`../neovim/`](../neovim/) and [`../eclipse/`](../eclipse/) cover the same transport for those -editors. +[`../neovim/`](../neovim/README.md) and [`../eclipse/`](../eclipse/README.md) cover the same +transport for those editors. diff --git a/plugin/docs/DEBUGGING.md b/plugin/docs/DEBUGGING.md index a9ae7fd..6ce70bd 100644 --- a/plugin/docs/DEBUGGING.md +++ b/plugin/docs/DEBUGGING.md @@ -184,7 +184,7 @@ Three things the IDE side is responsible for: order and carry no request id, which is only sound while every request is answered. After a timeout the agent may still be working, and its late reply would otherwise be handed to the next request as its own answer — so `BshDebugProcess` - marks the channel desynced instead. Correlating replies is the general fix and it + marks the channel desynced instead. Correlating replies is the general fix, and it belongs with threads, which need it anyway. Evaluation has its own, much longer timeout: a watch expression is arbitrary user diff --git a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumenter.kt b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumenter.kt index c910d42..3a7ca18 100644 --- a/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumenter.kt +++ b/plugin/src/main/kotlin/cz/loplex/intellij/bsh/debug/BshDebugInstrumenter.kt @@ -17,7 +17,7 @@ import cz.loplex.intellij.bsh.psi.BshElementTypes as E * hook is simply one extra statement executed just before the original one, which * keeps the transformation semantics-preserving. * - * The line number baked into each hook is chosen by [lineFor]. For a standalone + * The line number baked into each hook is chosen by the `lineFor` callback. For a standalone * `.bsh` file it is the statement's own line, so breakpoints map directly. For an * inline script injected into a pom.xml the caller bakes the **host pom.xml line** * instead, so every instrumented snippet in the build reports absolute pom lines to From 4b5c66097b5ca440f5fb50e6c4346a7130746f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Mon, 3 Aug 2026 17:09:06 +0200 Subject: [PATCH 13/28] Standardize prose to American English and clean up doc formatting Comments and docs had drifted between British and American spelling (behaviour/behavior, serialise/serialize, etc.); pick American consistently and rename the one file/function that carried the British spelling into its name, including the remaining -ise spellings a first pass missed. Reword "the code around them" to "the surrounding code" in BshDebugProtocol.kt, and rewrap a comment in BshDebugInstrumentation.kt that ran past the line-length convention. BshDebugAgent: reword "one that does gets a fresh id" -- grammatically fine but a garden-path parse -- to "one that reports again gets a fresh id". BshDebugProcess: split the lastStoppedThread doc comment's 42-word sentence in two at the "but" clause. Also fixes Markdown table formatting across the READMEs and docs. --- agent/README.md | 32 +++++----- ...-unchanged.sh => 04-behavior-unchanged.sh} | 18 +++--- agent/checks/07-dap-transport.sh | 2 +- agent/checks/README.md | 26 ++++---- agent/checks/dap-client.py | 2 +- .../main/java/cz/loplex/bsh/hook/BshHook.java | 18 +++--- .../java/cz/loplex/bsh/hook/DapChannel.java | 2 +- .../java/cz/loplex/bsh/hook/DebugChannel.java | 6 +- .../cz/loplex/bsh/hook/NativeChannel.java | 6 +- agent/samples/README.md | 58 ++++++++--------- docs/FUTURE_WORK.md | 6 +- docs/PROTOCOL.md | 52 +++++++-------- docs/RELEASING.md | 10 +-- editors/eclipse/README.md | 4 +- plugin/README.md | 16 ++--- plugin/docs/ARCHITECTURE.md | 10 +-- plugin/docs/DEBUGGING.md | 28 ++++---- .../bsh/debug/agent/BshDebugAgent.java | 10 +-- .../intellij/bsh/BshFileTypeDetector.kt | 2 +- .../cz/loplex/intellij/bsh/BshLanguage.kt | 2 +- .../bsh/debug/BshDebugInstrumentation.kt | 6 +- .../intellij/bsh/debug/BshDebugProcess.kt | 8 +-- .../intellij/bsh/debug/BshDebugProtocol.kt | 4 +- .../intellij/bsh/debug/BshDebugRunner.kt | 2 +- .../intellij/bsh/debug/BshJavaDebugAttach.kt | 2 +- .../bsh/debug/maven/BshMavenDebugSupport.kt | 4 +- .../debug/maven/BshMavenRunConfiguration.kt | 2 +- .../intellij/bsh/highlight/BshColors.kt | 2 +- .../cz/loplex/intellij/bsh/lexer/BshLexer.kt | Bin 9813 -> 9813 bytes .../loplex/intellij/bsh/parser/BshParser.kt | 2 +- plugin/tools/README.md | 60 ++++++++++++++++-- plugin/tools/mock-ide.py | 2 +- 32 files changed, 226 insertions(+), 178 deletions(-) rename agent/checks/{04-behaviour-unchanged.sh => 04-behavior-unchanged.sh} (81%) diff --git a/agent/README.md b/agent/README.md index 8ea225d..874de37 100644 --- a/agent/README.md +++ b/agent/README.md @@ -24,7 +24,7 @@ nested jar. The goal is debugging BeanShell inside **third-party libraries that already bundle bsh** — Maven plugins such as maven-enforcer being the motivating case. The code is -fixed; only runtime behaviour can be changed. That rules out patching BeanShell and +fixed; only runtime behavior can be changed. That rules out patching BeanShell and makes an agent the only vehicle. **JDWP is not usable, and the reason is a language property rather than an @@ -113,14 +113,14 @@ the system loader cannot define a second copy. are the init list, the condition, the update list *and* the body, all reporting the `for` line. The child layouts were read off real parse trees: - | node | children | statement position | - |---|---|---| - | `BSHIfStatement` | `[cond, then, else?]` | index ≥ 1 | - | `BSHWhileStatement` (`while`) | `[cond, body]` | last | - | `BSHWhileStatement` (`do`) | `[body, cond]` | **first** | - | `BSHForStatement` | `[init?, cond?, update?, body]` | last | - | `BSHEnhancedForStatement` | `[type?, iterable, body]` | last | - | `BSHSwitchStatement` | `[expr, label, stmt, …]` | index ≥ 1 | + | node | children | statement position | + |-------------------------------|---------------------------------|--------------------| + | `BSHIfStatement` | `[cond, then, else?]` | index ≥ 1 | + | `BSHWhileStatement` (`while`) | `[cond, body]` | last | + | `BSHWhileStatement` (`do`) | `[body, cond]` | **first** | + | `BSHForStatement` | `[init?, cond?, update?, body]` | last | + | `BSHEnhancedForStatement` | `[type?, iterable, body]` | last | + | `BSHSwitchStatement` | `[expr, label, stmt, …]` | index ≥ 1 | `do` and `while` are the *same node type* (`DoStatement() #WhileStatement`) with opposite child order, separated by the package-private `isDoStatement` field. @@ -138,9 +138,9 @@ the system loader cannot define a second copy. ### Not bit-transparent -Behaviour is unchanged — every fixture produces identical output with and without +Behavior is unchanged — every fixture produces identical output with and without the agent — but **identity hash codes shift** deterministically (`Point@279f2327` -becomes `Point@30f39991` and stays there), because initialising the hook on the +becomes `Point@30f39991` and stays there), because initializing the hook on the interpreter thread advances that thread's identity-hash sequence. Nothing correct depends on those values, but a script printing a default `toString()` shows different digits. @@ -190,7 +190,7 @@ valid until the next resume and the table is dropped there, so the IDE can never hold a reference into a script that has moved on — no stale-object problem to solve, no cleanup protocol to get wrong. That is [DAP's `variablesReference`](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable) -in a smaller encoding: adopting DAP later changes the serialisation, not the design. +in a smaller encoding: adopting DAP later changes the serialization, not the design. Requests are served **on the thread they concern**, from inside the same loop where it waits for `RESUME`. Not a shortcut: that thread is parked there anyway, it owns the @@ -218,9 +218,9 @@ does, so a filter is not optional: BeanShell's own commands (`print`, `pwd`, … `.bsh` files on the classpath, and without a filter the session stops inside `print.bsh` on every `print()` call. Two properties, ORed: -| property | match | for | -|---|---|---| -| `bsh.debug.sources` | comma-separated, `endsWith` | a script that has a file name | +| property | match | for | +|--------------------------|------------------------------------------------|--------------------------------------| +| `bsh.debug.sources` | comma-separated, `endsWith` | a script that has a file name | | `bsh.debug.sources.file` | a file of prefixes, one per line, `startsWith` | a script handed over as a **string** | The second exists because a string has no file name. BeanShell invents one: @@ -300,7 +300,7 @@ correct in the CLI and does nothing in a library. ./gradlew :agent:samples:runHostWithAgent # the same, under the agent ``` -The two must agree, which is what pins down "behaviour unchanged". The README +The two must agree, which is what pins down "behavior unchanged". The README there lists the three differences that are legitimate. The transport itself can be exercised without the IDE, and the instrumentation diff --git a/agent/checks/04-behaviour-unchanged.sh b/agent/checks/04-behavior-unchanged.sh similarity index 81% rename from agent/checks/04-behaviour-unchanged.sh rename to agent/checks/04-behavior-unchanged.sh index eec5cb9..a65cd32 100755 --- a/agent/checks/04-behaviour-unchanged.sh +++ b/agent/checks/04-behavior-unchanged.sh @@ -6,17 +6,17 @@ # comparison has to allow exactly three differences, all documented in agent/samples/README.md, and # allowing them by pattern rather than by eye is the point of automating it: # -# * identity hash codes shift (Point@279f2327 -> Point@30f39991), because initialising the hook on +# * identity hash codes shift (Point@279f2327 -> Point@30f39991), because initializing the hook on # the interpreter thread advances that thread's identity-hash sequence; # * the interleaving of the two threads in scenario 5 is not deterministic in either run; # * bsh.NameSpace@... addresses, for the same reason as the first. # -# Anything else differing means the agent changed behaviour, which is a bug however useful the +# Anything else differing means the agent changed behavior, which is a bug however useful the # debugger is. source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -banner "04 — behaviour is unchanged with the agent attached" +banner "04 — behavior is unchanged with the agent attached" if ! "$GRADLEW" -q -p "$REPO_ROOT" :agent:samples:runHost > "$CHECK_TMP/plain.txt" 2>"$CHECK_TMP/plain.err"; then fail "the uninstrumented fixtures run" "$(tail -3 "$CHECK_TMP/plain.err")" @@ -31,28 +31,28 @@ fi pass "both runs completed" -# Normalise the three legitimate differences away, then require equality. Sorting the thread-5 lines +# Normalize the three legitimate differences away, then require equality. Sorting the thread-5 lines # is what makes the interleaving irrelevant without hiding a missing line: a dropped or extra line # still changes the sorted text. -normalise() { +normalize() { sed -E \ -e 's/@[0-9a-f]{6,}/@HASH/g' \ -e 's/bsh\.NameSpace: [^ ]+ \(bsh\.NameSpace@HASH\)/bsh.NameSpace@HASH/g' \ "$1" | LC_ALL=C sort } -normalise "$CHECK_TMP/plain.txt" > "$CHECK_TMP/plain.norm" -normalise "$CHECK_TMP/agent.txt" > "$CHECK_TMP/agent.norm" +normalize "$CHECK_TMP/plain.txt" > "$CHECK_TMP/plain.norm" +normalize "$CHECK_TMP/agent.txt" > "$CHECK_TMP/agent.norm" if diff -q "$CHECK_TMP/plain.norm" "$CHECK_TMP/agent.norm" >/dev/null; then - pass "output is identical once identity hashes and thread interleaving are normalised" + pass "output is identical once identity hashes and thread interleaving are normalized" else fail "output differs beyond the three documented differences" \ "$(diff "$CHECK_TMP/plain.norm" "$CHECK_TMP/agent.norm" | head -20)" fi # A weaker but independent assertion: the same number of lines, which catches a fixture that silently -# stopped early under the agent even if normalisation were too generous. +# stopped early under the agent even if normalization were too generous. plain_lines=$(wc -l < "$CHECK_TMP/plain.txt") agent_lines=$(wc -l < "$CHECK_TMP/agent.txt") assert_equals "$plain_lines" "$agent_lines" "both runs produced the same number of output lines" diff --git a/agent/checks/07-dap-transport.sh b/agent/checks/07-dap-transport.sh index 2338de5..00ad08a 100755 --- a/agent/checks/07-dap-transport.sh +++ b/agent/checks/07-dap-transport.sh @@ -7,7 +7,7 @@ # keeps the native protocol, which check 03 and 05 cover. # # What this asserts is that a real DAP conversation works end to end -- the handshake in the right -# order, breakpoints honoured, a stack with more than one frame, both scopes, and an expression +# order, breakpoints honored, a stack with more than one frame, both scopes, and an expression # evaluated in the stopped frame. Between them those cover every translation the DAP channel makes, # and each one has a specific way of going wrong: # diff --git a/agent/checks/README.md b/agent/checks/README.md index 67f4877..92c2c6e 100644 --- a/agent/checks/README.md +++ b/agent/checks/README.md @@ -8,22 +8,22 @@ End-to-end checks for the debug agent, as standalone bash scripts. ``` Each script builds what it needs, prints one `PASS`/`FAIL` line per assertion, and exits -non-zero if any failed. `JAVA_HOME` is honoured; the agent targets Java 8, so anything +non-zero if any failed. `JAVA_HOME` is honored; the agent targets Java 8, so anything 8+ works for the debugged JVM. ## Why these are not Gradle tests Each one needs something a JVM test cannot arrange from inside itself: -| check | needs | -|---|---| -| `01-inline-eval-source-name.sh` | a JVM launched with `-javaagent`, so the interpreter is instrumented before it loads | -| `02-maven-plugin-realm.sh` | a **real `mvn` process**, because the thing under test is a Maven plugin's own classloader | -| `03-scopes-and-introspection.sh` | two processes and a socket between them — the actual wire protocol | -| `04-behaviour-unchanged.sh` | the same fixtures run twice, in separate JVMs, one with the agent | -| `05-two-script-threads.sh` | two real threads, suspended at the same time over one socket | -| `06-suspend-all.sh` | a thread stopping at a line that has no breakpoint on it | -| `07-dap-transport.sh` | a real DAP conversation, handshake included, over a socket | +| check | needs | +|----------------------------------|--------------------------------------------------------------------------------------------| +| `01-inline-eval-source-name.sh` | a JVM launched with `-javaagent`, so the interpreter is instrumented before it loads | +| `02-maven-plugin-realm.sh` | a **real `mvn` process**, because the thing under test is a Maven plugin's own classloader | +| `03-scopes-and-introspection.sh` | two processes and a socket between them — the actual wire protocol | +| `04-behavior-unchanged.sh` | the same fixtures run twice, in separate JVMs, one with the agent | +| `05-two-script-threads.sh` | two real threads, suspended at the same time over one socket | +| `06-suspend-all.sh` | a thread stopping at a line that has no breakpoint on it | +| `07-dap-transport.sh` | a real DAP conversation, handshake included, over a socket | They are also the checks you want *while* changing the agent, one at a time, with the output in front of you — which is the other reason they are scripts. @@ -64,14 +64,14 @@ only observation that distinguishes the round-up from ordinary per-thread suspen thread its own code so the breakpoint can belong to one of them alone. **`07` — the DAP transport.** That the same debugger works over DAP: the handshake in the right -order (a client that never sees `initialized` configures nothing), breakpoints honoured, a stack +order (a client that never sees `initialized` configures nothing), breakpoints honored, a stack with depth, both scopes, and an expression evaluated in the stopped frame. Between them those cover every translation `DapChannel` performs. `dap-client.py` beside it is a standalone DAP client for driving a session by hand, the way `mock-ide.py` is for the native protocol. -**`04` — behaviour is unchanged.** The agent must not change what a script does. Allows +**`04` — behavior is unchanged.** The agent must not change what a script does. Allows exactly the three differences documented in [`../samples/README.md`](../samples/README.md) -(identity hashes, `NameSpace` addresses, thread interleaving) by normalising them, and +(identity hashes, `NameSpace` addresses, thread interleaving) by normalizing them, and requires equality otherwise. ## Adding one diff --git a/agent/checks/dap-client.py b/agent/checks/dap-client.py index 113736c..aa92e3e 100755 --- a/agent/checks/dap-client.py +++ b/agent/checks/dap-client.py @@ -23,7 +23,7 @@ [--evaluate 'expr'] [--set name=value] [--json] Prints one line per protocol step, which is what the checks assert on. `--json` additionally -dumps every message, for when the disagreement is about the wire rather than the behaviour. +dumps every message, for when the disagreement is about the wire rather than the behavior. """ import argparse import json diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java index 3c610c0..7217223 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/BshHook.java @@ -116,7 +116,7 @@ public final class BshHook { /** * Upper bound on the children of one value. Lazy expansion removes the cost of unopened * objects, not the cost of an opened one, and a million-element list would still stall the - * interpreter thread while it serialised. + * interpreter thread while it serialized. */ private static final int MAX_CHILDREN = 1000; @@ -159,7 +159,7 @@ public final class BshHook { * indices. * * This is per-node-type knowledge, which the transformer deliberately avoids — but here it - * degrades gracefully. An unrecognised container simply reports none of its direct children, + * degrades gracefully. An unrecognized container simply reports none of its direct children, * exactly as before this rule existed, so a future BeanShell that renames or reshapes a node * loses brace-less-body coverage instead of misbehaving. */ @@ -270,7 +270,7 @@ private static final class ThreadState { /** * Whether every thread should report its next statement, whatever the breakpoints say. * - *

How Suspend: All is honoured without pretending to be JDWP. A thread cannot be frozen from + *

How Suspend: All is honored without pretending to be JDWP. A thread cannot be frozen from * outside — it only ever stops where it calls the hook — so "suspend all" is implemented as * "everyone reports at the next statement, and the IDE decides who stays stopped". The IDE sets * this when a Suspend: All breakpoint is hit and clears it on resume. @@ -442,7 +442,7 @@ public static void onEval(Object node, Object callstack, Object interpreter) { } report(state, line, sourceFile, callstack, interpreter); } catch (Throwable t) { - // Never let a debugging problem change the behaviour of the debugged script. + // Never let a debugging problem change the behavior of the debugged script. disabled = true; System.err.println("[bsh-agent] disabling instrumentation after an internal error: " + t); close(); @@ -668,7 +668,7 @@ private static void readerLoop() { /** * Applies one command on the thread that owns the state, and answers it if it expects an answer. * - *

Returns true when the thread should stop waiting. An unrecognised command counts as a + *

Returns true when the thread should stop waiting. An unrecognized command counts as a * release: the worst case is a script that keeps running, whereas ignoring it could leave a thread * parked for good. */ @@ -744,7 +744,7 @@ private static void sessionLost(IOException ex) { disabled = true; close(); // Release everyone parked on a mailbox, or a suspended thread would wait for an IDE that is - // gone. An empty message array is read as "unrecognised", which applyCommand treats as a + // gone. An empty message array is read as "unrecognized", which applyCommand treats as a // release. for (ThreadState waiting : threadsById.values()) { waiting.mailbox.offer(DebugChannel.Command.simple( @@ -796,7 +796,7 @@ private static boolean isReportedSource(String sourceFile) { * Reads the prefix list, or returns null when there is none to read. * *

An unreadable file returns null — "report everything" — rather than failing. A filter is an - * optimisation over reporting every statement and letting the IDE decide; losing it costs speed, + * optimization over reporting every statement and letting the IDE decide; losing it costs speed, * whereas throwing here would abort somebody's build over a missing temp file. */ private static String[] readSourcePrefixes(String path) { @@ -1247,7 +1247,7 @@ private static Outcome storeInMap(ThreadState state, Map map, St return Outcome.failed("No entry " + name + " in this map"); } // Written through the map rather than through Map.Entry.setValue, which not every - // implementation honours once iteration has finished. + // implementation honors once iteration has finished. map.put(key, value); return Outcome.of(state, map.get(key)); } @@ -1293,7 +1293,7 @@ private static String reason(Throwable error) { * stack trace on further lines. Only the tail of the first line belongs in an IDE error field. * *

The search for the echo's closing quotes starts past the expression, so quotes inside what - * the user typed cannot split the message in the wrong place. An unrecognised shape falls back + * the user typed cannot split the message in the wrong place. An unrecognized shape falls back * to the whole first line, which is verbose rather than wrong. */ private static String describe(Throwable error, String expression) { diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java index 00d20cc..91535cf 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/DapChannel.java @@ -285,7 +285,7 @@ public Command readCommand() throws IOException { return handleStep(seq, command, args); } if ("pause".equals(command)) { - // Cannot be honoured: a thread is only ever stopped where it calls the hook, so there is + // Cannot be honored: a thread is only ever stopped where it calls the hook, so there is // nothing to interrupt. Refusing with a reason beats accepting and doing nothing. sendResponse(seq, command, false, null, "Pause is not supported: a BeanShell thread can only stop at a statement"); diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java index 8ed581d..5d31f40 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/DebugChannel.java @@ -17,7 +17,7 @@ * the right way round even though DAP is the standard: the native one is the narrower of the two, so * everything it needs DAP can express, while the reverse is not true. Where DAP wants something the * hook does not track — a source reference, a variable's {@code evaluateName} — the DAP channel - * synthesises it rather than the hook learning about it. + * synthesizes it rather than the hook learning about it. */ interface DebugChannel { @@ -63,7 +63,7 @@ void sendEvaluated(int requestId, boolean setVariable, boolean ok, String value, /** * Blocks until the next command arrives, or returns null at end of stream. * - *

Called only from the hook's single reader thread, so it needs no synchronisation of its own. + *

Called only from the hook's single reader thread, so it needs no synchronization of its own. */ Command readCommand() throws IOException; @@ -107,7 +107,7 @@ final class Variable { } /** - * A command, normalised across the two encodings. + * A command, normalized across the two encodings. * *

Deliberately one flat type rather than a hierarchy: there are seven of them, each carrying at * most a handful of ints and a string or two, and the hook switches on {@link #kind} in one place. diff --git a/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java b/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java index 1ff98f8..01dfb8c 100644 --- a/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java +++ b/agent/hook/src/main/java/cz/loplex/bsh/hook/NativeChannel.java @@ -36,7 +36,7 @@ final class NativeChannel implements DebugChannel { private final int port; - /** Serialises writes. A message must go out under one acquisition or two threads' fields interleave. */ + /** Serializes writes. A message must go out under one acquisition or two threads' fields interleave. */ private final Object writeLock = new Object(); private Socket socket; @@ -134,8 +134,8 @@ public void sendEvaluated(int requestId, boolean setVariable, boolean ok, String * Reads one command. * *

Every opcode's shape has to be known even for the ones this method does not interpret: there - * are no length prefixes, so an opcode it could not decode would desynchronise the stream for - * good. An unrecognised one therefore reads its thread id and stops there, which keeps the stream + * are no length prefixes, so an opcode it could not decode would desynchronize the stream for + * good. An unrecognized one therefore reads its thread id and stops there, which keeps the stream * aligned for anything carrying no further fields. */ public Command readCommand() throws IOException { diff --git a/agent/samples/README.md b/agent/samples/README.md index 1dd2621..f865d4c 100644 --- a/agent/samples/README.md +++ b/agent/samples/README.md @@ -23,7 +23,7 @@ appears to work in the CLI and does nothing in an embedded library, or the rever ### Checking that the agent changes nothing -`runHost` and `runHostWithAgent` must produce the same output, after normalising +`runHost` and `runHostWithAgent` must produce the same output, after normalizing three things that legitimately differ: ```bash @@ -33,7 +33,7 @@ diff <(norm plain.txt) <(norm agent.txt) - The **JVM warning** about class-data sharing appears because the agent appends the hook jar to the bootstrap classpath. Unavoidable and harmless. -- **Identity hash codes** shift deterministically — initialising the hook on the +- **Identity hash codes** shift deterministically — initializing the hook on the interpreter thread advances that thread's identity-hash sequence. See "Not bit-transparent" in [`agent/README.md`](../README.md). - **Thread interleaving** in `06_callbacks_threads.bsh` varies between runs *without* @@ -41,37 +41,37 @@ diff <(norm plain.txt) <(norm agent.txt) ## Hook point coverage -| Hook point | Exercised by | -|---|---| -| `Interpreter.java:659` — eval loop | `DebugHost` 1, 2, 6; every `source()` | -| `Interpreter.java:471` — interactive loop | any script run via the CLI | -| `BSHBlock.java:105` — statement in a block | all, esp. `01_basic` BP:6, BP:8 | -| `BshMethod.java:204` — scripted method entry | `02_methods`, all of `04_closures` | -| `BshMethod.java:256` — `invokeImpl` under lock | `02_methods` BP:9 (`synchronized`) | -| `Name.java:885` — `invokeLocalMethod`, unqualified | `03_invocation_forms` group A | -| `Name.java:778` — `invokeMethod`, dotted name | `03_invocation_forms` group B | -| `BSHPrimarySuffix.java:170` — **bypasses `Name`** | `03_invocation_forms` group C | -| `Reflect.java:103` — the single `method.invoke()` | `03_invocation_forms` B and C | -| `Reflect.java:895` — compiled Java command | `03_invocation_forms` (uncomment `dir`) | -| `This.java:208` — scripted object / proxy entry | `04_closures`, `06_*`, `DebugHost` 3, 4 | +| Hook point | Exercised by | +|------------------------------------------------------|------------------------------------------| +| `Interpreter.java:659` — eval loop | `DebugHost` 1, 2, 6; every `source()` | +| `Interpreter.java:471` — interactive loop | any script run via the CLI | +| `BSHBlock.java:105` — statement in a block | all, esp. `01_basic` BP:6, BP:8 | +| `BshMethod.java:204` — scripted method entry | `02_methods`, all of `04_closures` | +| `BshMethod.java:256` — `invokeImpl` under lock | `02_methods` BP:9 (`synchronized`) | +| `Name.java:885` — `invokeLocalMethod`, unqualified | `03_invocation_forms` group A | +| `Name.java:778` — `invokeMethod`, dotted name | `03_invocation_forms` group B | +| `BSHPrimarySuffix.java:170` — **bypasses `Name`** | `03_invocation_forms` group C | +| `Reflect.java:103` — the single `method.invoke()` | `03_invocation_forms` B and C | +| `Reflect.java:895` — compiled Java command | `03_invocation_forms` (uncomment `dir`) | +| `This.java:208` — scripted object / proxy entry | `04_closures`, `06_*`, `DebugHost` 3, 4 | | `This.java:236` — `JAVACODE` substitution, line `-1` | `06_callbacks_threads`, `DebugHost` 3, 4 | -| `ClassGeneratorUtil.java:388` — bytecode shim | `05_scripted_class`, `DebugHost` 6 | -| fresh `CallStack` per thread | `06_callbacks_threads`, `DebugHost` 5 | -| `BSHTryStatement.java:173` — finally / ReturnControl | `08_exceptions` BP:5-8 | +| `ClassGeneratorUtil.java:388` — bytecode shim | `05_scripted_class`, `DebugHost` 6 | +| fresh `CallStack` per thread | `06_callbacks_threads`, `DebugHost` 5 | +| `BSHTryStatement.java:173` — finally / ReturnControl | `08_exceptions` BP:5-8 | ## Files -| File | Covers | Watch out for | -|---|---|---| -| `01_basic.bsh` | top-level statements, loops, block scoping | statements in loop bodies do **not** re-enter the `Interpreter` loop | -| `02_methods.bsh` | frames, recursion, `synchronized`, `throws` | redefinition does not replace — see the defects note below | -| `03_invocation_forms.bsh` | the three call forms | group C never touches `Name` | -| `04_closures.bsh` | `This`, closures, scope chain | variables live in *parent* namespaces, not one flat frame | -| `05_scripted_class.bsh` | generated bytecode + shim | constructors must be `public` — see the defects note below | -| `06_callbacks_threads.bsh` | proxies, threads, Java→script | outermost frame has line `-1` | -| `07_eval_and_source.bsh` + `07_aux.bsh` | `source()` vs `eval()` identity | `eval()` reparses every call | -| `08_exceptions.bsh` | TargetError / EvalError / parse error | `finally` eats return values — see the defects note below | -| `DebugHost.java` | 8 embedded scenarios | the realistic third-party-library entry pattern | +| File | Covers | Watch out for | +|-----------------------------------------|---------------------------------------------|----------------------------------------------------------------------| +| `01_basic.bsh` | top-level statements, loops, block scoping | statements in loop bodies do **not** re-enter the `Interpreter` loop | +| `02_methods.bsh` | frames, recursion, `synchronized`, `throws` | redefinition does not replace — see the defects note below | +| `03_invocation_forms.bsh` | the three call forms | group C never touches `Name` | +| `04_closures.bsh` | `This`, closures, scope chain | variables live in *parent* namespaces, not one flat frame | +| `05_scripted_class.bsh` | generated bytecode + shim | constructors must be `public` — see the defects note below | +| `06_callbacks_threads.bsh` | proxies, threads, Java→script | outermost frame has line `-1` | +| `07_eval_and_source.bsh` + `07_aux.bsh` | `source()` vs `eval()` identity | `eval()` reparses every call | +| `08_exceptions.bsh` | TargetError / EvalError / parse error | `finally` eats return values — see the defects note below | +| `DebugHost.java` | 8 embedded scenarios | the realistic third-party-library entry pattern | ## Three BeanShell defects these confirmed diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index d1975e1..a1dfbfb 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -71,7 +71,7 @@ event, so routing IntelliJ through DAP would have lost the thread support the na It came out cheap for the reason predicted here — only the last step of the hook turns an answer into bytes, so `DebugChannel` splits the transport off and everything above it is written once. Adopting DAP's vocabulary first (`stackTrace`/`scopes`/`variables`/`evaluate`/ -`setVariable`, handles as `variablesReference`) is what made it a serialisation change rather +`setVariable`, handles as `variablesReference`) is what made it a serialization change rather than a redesign. Three translations live in `DapChannel` and are worth knowing about: frame ids have to be made @@ -129,7 +129,7 @@ headless VS Code (`@vscode/test-electron` + Mocha, under Xvfb) starting an actua session against the fixture in `src/test/fixtures/workspace/`, asserting on the DAP traffic via a `DebugAdapterTracker`. The fixture, breakpoint line and evaluate expression are the same ones `07` already proved work over `DapChannel` — deliberately, so the two checks are provably -exercising the same behaviour one layer apart rather than two fixtures that could quietly drift. +exercising the same behavior one layer apart rather than two fixtures that could quietly drift. **The one real gap `07` cannot reach**, and the reason this is `launch` rather than `attach`: `dap-client.py` connects to a JVM the check already started, so it never touches this @@ -168,7 +168,7 @@ in [`editors/vscode/README.md`](../editors/vscode/README.md#testing). (`nvim --headless -l`, no display server needed) against the same fixture, breakpoint line and evaluate expression `agent/checks/07-dap-transport.sh` and the VS Code test already prove work over `DapChannel` — deliberately, for the same reason the VS Code fixture matches `07`'s: so the -three checks are provably exercising the same behaviour, not three fixtures that could drift. +three checks are provably exercising the same behavior, not three fixtures that could drift. Covers what `07`'s `dap-client.py` cannot: `bsh-dap.lua`'s own `launch()` (the `jobstart` spawn, the `DAP: listening` stdout watch), the Neovim counterpart to what the VS Code GUI test found for `BshDebugAdapterDescriptorFactory.launch()`. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index b74605d..a2d6df4 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -34,11 +34,11 @@ its first report. There is no handshake and no version negotiation — see Encoding is `java.io.DataOutputStream` / `DataInputStream`, so: -| notation | on the wire | -|---|---| -| `byte` | 1 byte | -| `int` | 4 bytes, **big-endian** (`writeInt`) | -| `utf` | `writeUTF`: 2-byte unsigned length, then modified UTF-8 | +| notation | on the wire | +|----------|---------------------------------------------------------| +| `byte` | 1 byte | +| `int` | 4 bytes, **big-endian** (`writeInt`) | +| `utf` | `writeUTF`: 2-byte unsigned length, then modified UTF-8 | Messages are **not length-prefixed**. Each is an opcode byte followed by fields whose count and types the opcode fixes, so a reader must consume exactly what the opcode @@ -132,7 +132,7 @@ count times: At most **1000** children are written for one handle. Lazy expansion removes the cost of unopened objects, not of an opened one, and a million-element list would still stall the -interpreter thread while it serialised. +interpreter thread while it serialized. `bsh.Primitive` — the wrapper around every scripted `int`, `boolean` and so on — is reported **unwrapped**: `type` is what `Primitive.getType()` says (`int`), and @@ -180,7 +180,7 @@ count times: int line ``` -An optimisation, not a requirement: until the IDE sends this at least once, the agent +An optimization, not a requirement: until the IDE sends this at least once, the agent reports **every** statement and the IDE decides. Once sent, the agent falls silent while running and speaks up only where a breakpoint matches — which is what makes a loop usable. @@ -196,7 +196,7 @@ byte 0x08 byte on 1 = every thread reports its next statement, 0 = back to normal filtering ``` -Global, like `SET_BREAKPOINTS`, and how **Suspend: All** is honoured. The IDE sets it when a +Global, like `SET_BREAKPOINTS`, and how **Suspend: All** is honored. The IDE sets it when a breakpoint whose policy is `ALL` is hit, and clears it on resume. There is no way to freeze a thread from outside — it only ever stops where it calls the @@ -324,12 +324,12 @@ The agent is configured by **system properties**, not by the protocol, because t is loaded by the bootstrap classloader and system properties are the one channel that is loader-independent. -| property | meaning | -|---|---| -| `bsh.debug.port` | the IDE's listening port. Absent = not debugging, run untouched | -| `bsh.debug.sources` | comma-separated file-name suffixes to report on | -| `bsh.debug.sources.file` | path to a file of source-name **prefixes**, one per line | -| `bsh.debug.trace` | report to stderr instead of the socket (development aid) | +| property | meaning | +|--------------------------|-----------------------------------------------------------------| +| `bsh.debug.port` | the IDE's listening port. Absent = not debugging, run untouched | +| `bsh.debug.sources` | comma-separated file-name suffixes to report on | +| `bsh.debug.sources.file` | path to a file of source-name **prefixes**, one per line | +| `bsh.debug.trace` | report to stderr instead of the socket (development aid) | The two source filters are ORed; neither set means report everything. A filter is not optional under the instrumenting agent: it also reaches BeanShell's own commands, which @@ -339,23 +339,23 @@ no file name — see [`agent/README.md`](../agent/README.md#which-sources-to-rep ## 8. Failure modes -| situation | agent behaviour | -|---|---| -| no `bsh.debug.port` | disables itself; the script runs untouched | -| port set, nothing listening | `System.exit(69)` (`EX_UNAVAILABLE`) — silently skipping every breakpoint is the failure that looks like "it just ran" | -| session drops mid-run | warn and detach; the script continues. Aborting what may be a real Maven build would be worse | -| a request fails (bad expression) | ordinary reply with `ok = 0`; the connection stays up | -| an unreadable object | send whatever was gathered; not worth failing the session | -| reflection setup fails | give up reporting, permanently, rather than throwing from inside a transformer | +| situation | agent behavior | +|----------------------------------|------------------------------------------------------------------------------------------------------------------------| +| no `bsh.debug.port` | disables itself; the script runs untouched | +| port set, nothing listening | `System.exit(69)` (`EX_UNAVAILABLE`) — silently skipping every breakpoint is the failure that looks like "it just ran" | +| session drops mid-run | warn and detach; the script continues. Aborting what may be a real Maven build would be worse | +| a request fails (bad expression) | ordinary reply with `ok = 0`; the connection stays up | +| an unreadable object | send whatever was gathered; not worth failing the session | +| reflection setup fails | give up reporting, permanently, rather than throwing from inside a transformer | ## 9. Relationship to DAP Two transports, chosen at premain by `bsh.debug.protocol`: -| value | transport | direction | who uses it | -|---|---|---|---| -| `native` (default) | this document | agent **connects** to the IDE's port (`bsh.debug.port`) | the IntelliJ plugin | -| `dap` | [Debug Adapter Protocol][dap] | agent **listens** (`bsh.debug.listen`, defaults to `bsh.debug.port`) | VS Code, Neovim, Eclipse, … | +| value | transport | direction | who uses it | +|--------------------|-------------------------------|----------------------------------------------------------------------|-----------------------------| +| `native` (default) | this document | agent **connects** to the IDE's port (`bsh.debug.port`) | the IntelliJ plugin | +| `dap` | [Debug Adapter Protocol][dap] | agent **listens** (`bsh.debug.listen`, defaults to `bsh.debug.port`) | VS Code, Neovim, Eclipse, … | **Why both, rather than DAP replacing this.** LSP4IJ's DAP client does not implement the `thread` event, so routing IntelliJ through DAP would *lose* the thread support the native diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 3995050..a48e1d2 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -3,11 +3,11 @@ One tag drives three independently-gated artifacts, all via [`.github/workflows/release.yml`](../.github/workflows/release.yml): -| Artifact | Changelog gate | Built by | Published to | -|---|---|---|---| -| IDEA plugin | `plugin/CHANGELOG.md` | `:plugin:buildPlugin` | GitHub Release + JetBrains Marketplace | -| Debug agent | `agent/CHANGELOG.md` | `:agent:instrument:shadowJar` | GitHub Release only | -| VS Code extension | `editors/vscode/CHANGELOG.md` | `vsce package` | GitHub Release + VS Code Marketplace | +| Artifact | Changelog gate | Built by | Published to | +|-------------------|-------------------------------|-------------------------------|----------------------------------------| +| IDEA plugin | `plugin/CHANGELOG.md` | `:plugin:buildPlugin` | GitHub Release + JetBrains Marketplace | +| Debug agent | `agent/CHANGELOG.md` | `:agent:instrument:shadowJar` | GitHub Release only | +| VS Code extension | `editors/vscode/CHANGELOG.md` | `vsce package` | GitHub Release + VS Code Marketplace | Pushing `vX.Y.Z` runs all three build jobs, but **each one only builds and publishes if its own changelog has a `## [X.Y.Z]` section.** An artifact with nothing new this diff --git a/editors/eclipse/README.md b/editors/eclipse/README.md index 568544e..bb49d97 100644 --- a/editors/eclipse/README.md +++ b/editors/eclipse/README.md @@ -88,8 +88,8 @@ expression `07` and the other two editors' tests already prove work): - resuming lets the script run to completion (`script done` on the target JVM's stdout) — and, since `DapChannel` never sends a `terminated`/`exited` DAP event, watch what LSP4E itself does when the socket merely drops: whether the Debug view marks the session - terminated on its own or is left stuck, since that is LSP4E's behaviour to characterize, not + terminated on its own or is left stuck, since that is LSP4E's behavior to characterize, not this agent's to fix. A step that stops holding is a regression in the DAP transport itself — cross-check against `07` -and the VS Code/Neovim tests before assuming it's LSP4E's own behaviour that changed. +and the VS Code/Neovim tests before assuming it's LSP4E's own behavior that changed. diff --git a/plugin/README.md b/plugin/README.md index 7da5e00..0257e40 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -147,14 +147,14 @@ launch it in Debug): ## Screenshots -| | | -|---|---| -| ![Editor](docs/images/editor.png) | ![Completion](docs/images/completion.png) | -| Syntax highlighting & Structure view | Code completion (keywords + in-scope names) | -| ![Navigation](docs/images/navigation.png) | ![Debugger](docs/images/debugger.png) | -| Quick documentation into Java on a chained member | Debugger: variables + console at a breakpoint | -| ![Maven injection](docs/images/maven-injection.png) | ![Inspection](docs/images/inspection.png) | -| BeanShell injected into `pom.xml` | Inspection quick-fix for an unused variable | +| | | +|-----------------------------------------------------|-----------------------------------------------| +| ![Editor](docs/images/editor.png) | ![Completion](docs/images/completion.png) | +| Syntax highlighting & Structure view | Code completion (keywords + in-scope names) | +| ![Navigation](docs/images/navigation.png) | ![Debugger](docs/images/debugger.png) | +| Quick documentation into Java on a chained member | Debugger: variables + console at a breakpoint | +| ![Maven injection](docs/images/maven-injection.png) | ![Inspection](docs/images/inspection.png) | +| BeanShell injected into `pom.xml` | Inspection quick-fix for an unused variable | ## Known limitations diff --git a/plugin/docs/ARCHITECTURE.md b/plugin/docs/ARCHITECTURE.md index 2516026..bda2cb0 100644 --- a/plugin/docs/ARCHITECTURE.md +++ b/plugin/docs/ARCHITECTURE.md @@ -73,11 +73,11 @@ Everything is registered in [`META-INF/plugin.xml`](../src/main/resources/META-I Optional integrations are isolated in separate descriptors loaded via optional ``: -| Descriptor | Loaded when | Provides | -|------------|-------------|----------| -| `bsh-java.xml` | Java plugin present | Java navigation / debug attach | -| `bsh-xml.xml` | XML module present | Maven/XML language injection | -| `bsh-spellchecker.xml` | Spellchecker present | comment/string spell-checking | +| Descriptor | Loaded when | Provides | +|------------------------|----------------------|--------------------------------| +| `bsh-java.xml` | Java plugin present | Java navigation / debug attach | +| `bsh-xml.xml` | XML module present | Maven/XML language injection | +| `bsh-spellchecker.xml` | Spellchecker present | comment/string spell-checking | Packages by concern: `highlight`, `editor` (folding, brace matcher, commenter, surround), `formatting`, `structure`, `navigation`, `completion`, `inspection`, diff --git a/plugin/docs/DEBUGGING.md b/plugin/docs/DEBUGGING.md index 6ce70bd..fecb509 100644 --- a/plugin/docs/DEBUGGING.md +++ b/plugin/docs/DEBUGGING.md @@ -16,15 +16,15 @@ There are two runtime mechanisms and one verification oracle. The mechanism is a per-configuration setting — *Debug instrumentation* in the BeanShell run configuration — defaulting to `AGENT`. -| Implementation | Role | -|---|---| -| `agent/` (JVM agent) | **Default.** Instruments the interpreter; the script is untouched | -| `debug/BshDebugInstrumenter.kt` (PSI) | Fallback. Needs only a source file — no agent, no JVM flag | -| `tools/bshInstrumenter.main.kts` | **Not a runtime option.** A verification oracle | +| Implementation | Role | +|---------------------------------------|-------------------------------------------------------------------| +| `agent/` (JVM agent) | **Default.** Instruments the interpreter; the script is untouched | +| `debug/BshDebugInstrumenter.kt` (PSI) | Fallback. Needs only a source file — no agent, no JVM flag | +| `tools/bshInstrumenter.main.kts` | **Not a runtime option.** A verification oracle | The setting is stored by enum **name** rather than ordinal, so reordering `BshInstrumentationMode` cannot silently repoint saved configurations at the other -mechanism, and an unrecognised value falls back to the default rather than refusing +mechanism, and an unrecognized value falls back to the default rather than refusing to launch. Choosing `AGENT` with no agent jar available also falls back to rewriting: the user asked to debug, and a degraded session beats none. @@ -117,12 +117,12 @@ variables. The reported BeanShell call depth is monotonic per nested call. Step actions compare against the depth captured when the step began: -| Action | Pauses when | -|--------|-------------| -| Step Into | next statement, any depth | +| Action | Pauses when | +|-----------|----------------------------------------------------------| +| Step Into | next statement, any depth | | Step Over | depth ≤ the step-point depth (skips descents into calls) | -| Step Out | depth < the step-point depth (only after returning) | -| Resume | only at a breakpoint | +| Step Out | depth < the step-point depth (only after returning) | +| Resume | only at a breakpoint | ## Java breakpoints (dual session) — `debug/BshDebugRunner.kt` @@ -155,7 +155,7 @@ then for a value's children when the user expands it. `BshStackFrame` and `BshDebugProcess` implements `BshValueSource` to make it. Nested objects, collections, maps and arrays therefore expand in the Variables -panel, and a value nobody looks at is never serialised. +panel, and a value nobody looks at is never serialized. **The two mechanisms differ here**, and the protocol lets them say so. The agent is handed the whole `CallStack`, so it reports every frame. The rewriting fallback is @@ -224,7 +224,7 @@ It prints the stack at each stop and, with `--expand`, opens every expandable va level — which is how the scopes and the `This`-as-namespace expansion above were checked. `--eval` and `--set` exercise the other two requests. -**Is behaviour unchanged?** `runHost` and `runHostWithAgent` must agree: +**Is behavior unchanged?** `runHost` and `runHostWithAgent` must agree: ```bash ./gradlew :agent:samples:runHost > plain.txt @@ -238,7 +238,7 @@ through to the JVM the plugin realm lives in. Debugging an inline ` + greet @@ -25,17 +36,6 @@ run - - - diff --git a/plugin/samples/maven/build-helper/pom.xml b/plugin/samples/maven/build-helper/pom.xml index 5866efa..76224ea 100644 --- a/plugin/samples/maven/build-helper/pom.xml +++ b/plugin/samples/maven/build-helper/pom.xml @@ -17,23 +17,28 @@ org.codehaus.mojo build-helper-maven-plugin 3.6.0 + + + + buildTag + + - compute-timestamp + default-cli validate bsh-property - - - - buildTag - - From 6e79f2438ff50f5fbdc2eb84f5f43ed31b3b0036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Tue, 4 Aug 2026 12:59:19 +0200 Subject: [PATCH 25/28] Add a screenshots gallery to the top-level README Mirrors plugin/README.md's screenshots, cycling through them as a single animated GIF with clickable thumbnails below -- GitHub's markdown sanitizer strips