Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
62731d4
Disable the bundled Vue.js plugin in the test sandbox
loplex Jul 31, 2026
828bd73
Fix findings from wiring up verifyPluginProjectConfiguration
loplex Aug 3, 2026
bd273e6
Rewrite the Marketplace description and add a preview task
loplex Aug 1, 2026
96ff935
Bump mavenExt's maven-core and plexus-utils past their flagged CVEs
loplex Aug 2, 2026
30c1a64
Apply mechanical Java modernization fixes from an IDE inspection sweep
loplex Aug 1, 2026
e162398
Apply mechanical Kotlin modernization fixes from an IDE inspection sweep
loplex Aug 1, 2026
563ed60
Remove dead code from the hook module and plugin
loplex Aug 3, 2026
e53f6c5
Fix real findings flagged by inspections across the hook module and p…
loplex Aug 3, 2026
a275786
Fix UI-facing strings and add missing description resources
loplex Aug 3, 2026
1434d0c
Suppress warnings for design-intentional patterns across the plugin a…
loplex Aug 3, 2026
77fbf49
Fix warnings in check-runner and dev tool scripts
loplex Aug 3, 2026
8701703
Fix broken doc links and stale references across the codebase
loplex Aug 3, 2026
4b5c660
Standardize prose to American English and clean up doc formatting
loplex Aug 3, 2026
8526172
Migrate parameter-name hints off the experimental InlayParameterHints…
loplex Aug 3, 2026
78bf1d0
Extract plugin.xml and live-template display text into BshBundle for …
loplex Aug 3, 2026
2a95145
Rename IntelliJ plugin
loplex Aug 3, 2026
6b98b6d
Refresh the README screenshots for the current demo project and New UI
loplex Aug 2, 2026
8403c94
Record three resolution gaps found by BshUnresolvedMethodInspection
loplex Aug 3, 2026
545db5c
Fix duplicate variables in the debugger's flattened frame children
loplex Aug 3, 2026
08daaf8
Show Locals/Global as labeled groups in the Variables view, with tests
loplex Aug 3, 2026
c7b1fe6
Extract and test the debug session's wire decoding
loplex Aug 3, 2026
2247fbf
Fix missing icon on the Create BeanShell-Enhanced Maven Configuration…
loplex Aug 4, 2026
cef8f17
Show nested Block/Closure scopes in the Variables view, with tests
loplex Aug 4, 2026
3e69890
Refresh README screenshots for the nested-scope Variables view
loplex Aug 4, 2026
6e79f24
Add a screenshots gallery to the top-level README
loplex Aug 4, 2026
ed37c5a
Resolve plugin.xml's screenshot URLs to the release tag at build time
loplex Aug 4, 2026
5f1d494
Use a real JDK 8 toolchain everywhere JDK 8 bytecode is compiled
loplex Aug 4, 2026
1b0eba6
Cut the plugin and agent changelogs for 0.2.0
loplex Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,24 @@ serializations of the same instrumentation underneath.
script calls into, for free, wherever the platform already wraps the JVM (e.g. debugging
an inline Maven script).

Full details, screenshots and known limitations: [`plugin/README.md`](plugin/README.md).
Full details and known limitations: [`plugin/README.md`](plugin/README.md).

### Screenshots

![BeanShell plugin screenshots](plugin/docs/images/showcase.gif)

Click a thumbnail to open it full-size:

<p>
<a href="plugin/docs/images/editor.png"><img src="plugin/docs/images/editor.png" width="120" alt="Editor" title="Syntax highlighting &amp; Structure view"></a>
<a href="plugin/docs/images/completion.png"><img src="plugin/docs/images/completion.png" width="120" alt="Completion" title="Code completion (keywords + in-scope names)"></a>
<a href="plugin/docs/images/navigation.png"><img src="plugin/docs/images/navigation.png" width="120" alt="Navigation" title="Quick documentation into Java on a chained member"></a>
<a href="plugin/docs/images/debugger.png"><img src="plugin/docs/images/debugger.png" width="120" alt="Debugger" title="Debugger: variables + console at a breakpoint"></a>
<a href="plugin/docs/images/maven-injection.png"><img src="plugin/docs/images/maven-injection.png" width="120" alt="Maven injection" title="BeanShell injected into pom.xml"></a>
<a href="plugin/docs/images/maven-completion.png"><img src="plugin/docs/images/maven-completion.png" width="120" alt="Maven completion" title="Completion inside an injected Maven &lt;script&gt;"></a>
<a href="plugin/docs/images/inspection.png"><img src="plugin/docs/images/inspection.png" width="120" alt="Inspection" title="Inspection quick-fix for an unused variable"></a>
<a href="plugin/docs/images/maven-debug.png"><img src="plugin/docs/images/maven-debug.png" width="120" alt="Maven debugger" title="Debugger: nested scopes through a Maven build"></a>
</p>

## Editors: VS Code, Neovim, Eclipse

Expand Down
9 changes: 9 additions & 0 deletions agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ VS Code, Neovim and Eclipse DAP transports. The copy of this jar bundled inside
IDEA plugin has its own release cycle -- see [`../plugin/CHANGELOG.md`](../plugin/CHANGELOG.md).

## [Unreleased]

## [0.2.0] - 2026-08-04

### Added

- `SCOPES` now reports a `Block`/`Closure` level for each `for`/`if` body or captured
closure namespace between a frame's `Locals` and `Global`, each carrying only its own
directly-declared variables, instead of `Locals` flattening the whole parent chain into
one group. See [`docs/PROTOCOL.md`](../docs/PROTOCOL.md#0x11-scopes--answers-0x04).
32 changes: 16 additions & 16 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion agent/checks/02-maven-plugin-realm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ MAVEN_OPTS="-javaagent:$AGENT_JAR -Dbsh.debug.trace=1" \
mvn -o -q -f "$POM" validate > "$CHECK_TMP/bh.txt" 2>&1
grep 'bsh-agent' "$CHECK_TMP/bh.txt" > "$CHECK_TMP/bh-agent.txt" || true

assert_contains "$CHECK_TMP/bh-agent.txt" 'src=inline evaluation of: ``prefix = project.getArtifactId();' \
assert_contains "$CHECK_TMP/bh-agent.txt" 'src=inline evaluation of: ``name = project.artifactId + ":" + project.version;' \
"build-helper: the inline <source> is instrumented inside the plugin realm" "$CHECK_TMP/bh.txt"
assert_contains "$CHECK_TMP/bh-agent.txt" 'line=1 src=inline evaluation of' \
"build-helper: lines are snippet-relative (first statement is line 1)" "$CHECK_TMP/bh.txt"
Expand Down
51 changes: 47 additions & 4 deletions agent/checks/03-scopes-and-introspection.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
# Drives the real transport -- mock-ide.py is the IDE end -- so this covers the socket conversation
# as well as the values, which no unit test on either side does alone.
#
# The two things asserted here are easy to regress invisibly. A bsh.This must expand to the
# The three things asserted here are easy to regress invisibly. A bsh.This must expand to the
# *namespace* it stands for rather than to its Java fields (that is what makes a closure's captured
# scope, a scripted instance's _bshThis... field, and a This handed back to Java all readable), and
# Global must appear when stopped inside a method, since that is where a script's top-level state
# would otherwise become invisible.
# scope, a scripted instance's _bshThis... field, and a This handed back to Java all readable), Global
# must appear when stopped inside a method, since that is where a script's top-level state would
# otherwise become invisible, and a `for` loop's own namespace must appear as its own level rather
# than being lost inside Locals' or absorbed into Global.

source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"

Expand Down Expand Up @@ -79,4 +80,46 @@ assert_contains "$CHECK_TMP/ide.txt" 'count = 0 (int)' \
assert_not_contains "$CHECK_TMP/ide.txt" 'declaringInterpreter' \
"expanding a This does not leak bsh.XThis's own Java fields"

# --- a `for` loop's own namespace is its own scope level ------------------------------------------
#
# BSHForStatement wraps the loop in a BlockNameSpace of its own (holding the init variable) and the
# body runs in a second, subordinate BlockNameSpace -- two levels below the script's own Locals used
# to be flattened into one, hiding a typed loop variable declared in the `for`'s own init behind
# whichever scope's ancestor-walk happened to reach it first.

cat > "$CHECK_TMP/forloop.bsh" <<'EOF'
total = 0;
for (int i = 1; i <= 3; i++) {
total += i;
print("step " + i);
}
EOF

PORT2=$((20000 + RANDOM % 20000))
python3 "$REPO_ROOT/plugin/tools/mock-ide.py" "$PORT2" \
--breakpoints forloop.bsh:3 --expand > "$CHECK_TMP/for-ide.txt" 2>&1 &
FOR_IDE_PID=$!

for _ in $(seq 50); do
grep -q 'listening on' "$CHECK_TMP/for-ide.txt" 2>/dev/null && break
sleep 0.1
done

"$JAVA" -javaagent:"$AGENT_JAR" -Dbsh.debug.port="$PORT2" -Dbsh.debug.sources=forloop.bsh \
-cp "$BSH_CLASSPATH" bsh.Interpreter "$CHECK_TMP/forloop.bsh" \
> "$CHECK_TMP/for-script.txt" 2>&1
wait "$FOR_IDE_PID" 2>/dev/null || true

assert_contains "$CHECK_TMP/for-ide.txt" 'Block:' \
"the for-loop's own namespace is offered as a level of its own"
assert_contains "$CHECK_TMP/for-ide.txt" 'i = 1 (int)' \
"the loop variable is visible, in the Block level it was actually declared in" "$CHECK_TMP/for-ide.txt"

# Global's own slice of the report, isolated so the assertion below cannot pass just because "i"
# legitimately appears a few lines up, under Block.
sed -n '/ Global:/,/^\[mock-ide\] STOPPED\|^\[mock-ide\] agent disconnected/p' "$CHECK_TMP/for-ide.txt" \
> "$CHECK_TMP/for-global-only.txt"
assert_not_contains "$CHECK_TMP/for-global-only.txt" 'i = 1 (int)' \
"the loop variable is not repeated in Global -- each level reports only its own" "$CHECK_TMP/for-ide.txt"

finish
Original file line number Diff line number Diff line change
Expand Up @@ -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")"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion agent/checks/07-dap-transport.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
#
Expand Down
26 changes: 13 additions & 13 deletions agent/checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions agent/checks/dap-client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions agent/checks/run-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading