Skip to content

Replace the Scala threading instrumenter with IntelliJ's TMH Java sources - #153

Merged
Vasil Vasilev (vasilmkd) merged 3 commits into
masterfrom
threading-instrumentation
Jul 31, 2026
Merged

Replace the Scala threading instrumenter with IntelliJ's TMH Java sources#153
Vasil Vasilev (vasilmkd) merged 3 commits into
masterfrom
threading-instrumentation

Conversation

@vasilmkd

Copy link
Copy Markdown
Contributor

Summary

This PR replaces the hand-written Scala port of the threading annotation instrumentation
(#SCL-18660) with the original Java sources from IntelliJ IDEA's JPS build, copied verbatim —
the same approach already used for the @NotNull instrumentation (#SCL-22085).

The Scala port (Feb 2, 2024) had drifted from upstream: three days after it was written,
IntelliJ's JPS switched to generating ThreadingAssertions calls
(IJPL-255) and chose softAssertReadAccess
for @RequiresReadLock, while our port generates the hard assertReadAccess. Copying the
upstream implementation removes this drift and the maintenance burden of keeping a parallel
port in sync.

⚠️ Behavior change

Matching IntelliJ IDEA's own instrumentation, @RequiresReadLock violations now log an
error instead of throwing an exception
(ThreadingAssertions.softAssertReadAccess). This
may change what CI reports for read-lock issues in projects using
instrumentThreadingAnnotations := true. All other annotations still throw as before.

Implementation

  • TMHInstrumenter, TMHAssertionGenerator, and TMHAssertionGenerator2 are copied
    verbatim from IntelliJ IDEA Community Edition (Apache 2.0, commit 7a35a7d7fe64 — the
    same commit the notNullVerification sources were copied from). The only changes are the
    package name, the ASM package, Opcodes.API_VERSIONOpcodes.ASM9, and the
    FailSafeMethodVisitor import (reusing the copy we already have).
    TMHAssertionGenerator1 (the legacy fallback for SDKs without ThreadingAssertions) is
    not copied — the previous implementation already required ThreadingAssertions, so
    nothing regresses.
  • ThreadingAnnotationInstrumenter.scala shrinks from ~106 lines of ASM visitor logic to a
    thin wrapper around TMHInstrumenter. Same object name and signature, so
    ManipulateBytecode and the instrumentThreadingAnnotations setting are unchanged.
  • org.jetbrains:annotations moves from Test to compile scope, since the copied sources use
    @ApiStatus.Internal/@NotNull.
  • Minor behavior fix for free: methods opted out via generateAssertion = false no longer
    trigger a pointless no-op rewrite of their class file.

Tests

The fixture harness from NotNullInstrumenterTest is extracted into a shared
InstrumenterTestHarness trait, and a new ThreadingAnnotationInstrumenterTest (5 tests)
compiles stub com.intellij.util.concurrency classes at test runtime whose assertion
methods record their invocations. It pins the annotation → assertion mapping (including
@RequiresReadLocksoftAssertReadAccess), assertion-before-body ordering, the
generateAssertion = false opt-out, no-op behavior on unannotated classes, and the
one-assertion-per-method behavior for doubly-annotated methods (which matches upstream's
last-annotation-wins semantics).

Vasil Vasilev (vasilmkd) and others added 3 commits July 31, 2026 11:25
…rces

The Scala port of the threading annotation instrumentation (SCL-18660,
Feb 2, 2024) drifted from upstream: three days after the port, IntelliJ's
JPS switched to generating ThreadingAssertions calls (IJPL-255) and chose
softAssertReadAccess (logs an error instead of throwing) for
@RequiresReadLock, while our port generates the hard assertReadAccess.

Copy TMHInstrumenter, TMHAssertionGenerator and TMHAssertionGenerator2
verbatim from IntelliJ IDEA Community Edition (Apache 2.0, commit
7a35a7d7fe64, the same commit the notNullVerification sources were copied
from), with only the package, the ASM package, Opcodes.API_VERSION ->
Opcodes.ASM9 and the FailSafeMethodVisitor import changed.
TMHAssertionGenerator1 (the legacy pre-ThreadingAssertions fallback) is
not needed and therefore not copied.

ThreadingAnnotationInstrumenter.scala becomes a thin wrapper around
TMHInstrumenter, so ManipulateBytecode is unchanged. The org.jetbrains
annotations dependency moves to compile scope because the copied sources
use @ApiStatus.Internal and @NotNull.

Behavior change: @RequiresReadLock violations now log an error instead of
throwing, matching IntelliJ IDEA's own instrumentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Byte-level comparison of the Scala Plugin built by sbt against the same
plugin built by IntelliJ IDEA's JPS showed that JPS adds a LineNumberTable
entry covering each injected ThreadingAssertions call, while this plugin
did not (the only remaining difference in threading-instrumented classes).

The standalone JPS builder (TMHInstrumentingBuilder) defaults
"tmh.generate.line.numbers" to false, which generateLineNumbers = false
mirrored. However, IDE-driven builds behave differently: the DevKit plugin
(TMHBuildProcessParametersProvider) injects -Dtmh.generate.line.numbers=true
into every JPS build process it spawns, because the corresponding registry
key defaults to true (intellij.devkit.core.xml). In practice, plugin
projects built by IntelliJ IDEA are always instrumented with line numbers.

Passing generateLineNumbers = true therefore matches IDEA's real output:
the injected assertion is attributed to the first line number of the
method, producing better stack traces when the assertion throws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins generateLineNumbers = true in ThreadingAnnotationInstrumenter, which
could otherwise look like a safe "simplification" back to false to a reader
who only checks the standalone JPS builder default. The test asserts, on
the ASM event stream of the instrumented method, that the injected
ThreadingAssertions call is preceded by a line number entry carrying the
first line number of the original method, matching JPS builds spawned by
IntelliJ IDEA. Verified to fail if generateLineNumbers is flipped back to
false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vasilmkd
Vasil Vasilev (vasilmkd) merged commit 7ea5c61 into master Jul 31, 2026
1 check passed
@vasilmkd
Vasil Vasilev (vasilmkd) deleted the threading-instrumentation branch July 31, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant