Running a Reviewable BigDecimal Migration with IntelliJ Structural Search
Blind text replacement rewrites comments and strings. IntelliJ's Structural Search and Replace matches the PSI tree instead, letting you migrate BigDecimal constructors with preview, constraints, and a clean VCS diff.
22 Dec 2025, 19:13 UTC

Why text search fails at API migrations
When a library deprecates a constructor or renames a method, the migration usually arrives as a list of hundreds of call sites. A plain text search-and-replace feels fast until it rewrites a string literal, a comment, or an unrelated variable that happens to share the same characters. Even with word boundaries, a lexical search cannot tell a real constructor invocation from a method named newBigDecimal or a log message that quotes the old API.
IntelliJ IDEA's Structural Search and Replace (SSR) takes a different approach. It queries the Program Structure Interface (PSI) tree—the parsed representation the IDE uses for completion, inspections, and refactorings—so you match syntax, not bytes. For a single-repository, mechanical API change, that turns a risky global edit into a reviewable pull request.
Matching structure, not text
An SSR template looks like a code snippet with variables. A variable such as $x$ matches any expression, and you can attach constraints to it: a required type, a regular expression on the text, or an occurrence count. Because the match happens on the PSI tree, the template new BigDecimal($x$) only fires on actual constructor calls, never inside strings or comments.
Scope matters as much as the template. Running SSR across an entire project can surface generated code or third-party sources that you do not intend to touch. Narrow the scope to the module or source root you own before you preview anything.
Worked example: migrating new BigDecimal(double)
The BigDecimal(double) constructor is unpredictable for binary floating-point values, so teams often migrate to BigDecimal.valueOf(double). The change is mechanical, but the text new BigDecimal( also appears in documentation, tests, and unrelated helpers. SSR handles the distinction cleanly.
1. Search read-only first
In the IDE, open Edit > Find > Find Structurally.... Menu labels have moved between IntelliJ releases, so confirm the exact path in your build. Enter the template:
new BigDecimal($x$)
Select the $x$ variable and add a constraint that its type is double. Set the scope to a single module rather than the whole project, then run the search. You should see only constructor invocations whose argument is a double expression.
Expected check: compare this match count with a plain text search for new BigDecimal(. The text search will usually return more hits; the difference is roughly the noise you just avoided.
2. Replace with preview
Switch to Replace Structurally... using the same search template and the replacement template:
BigDecimal.valueOf($x$)
Open the preview pane. Every selected match is shown as a diff hunk. Deselect anything that looks wrong—perhaps a test helper that intentionally constructs a BigDecimal from a double to assert an exception. Replacement applies to all selected matches at once; there is no per-match edit step, so over-broad constraints are the main risk.
Required permissions and risk: you need write access to the project files. Because the operation changes source files directly, work on a clean version control system (VCS) branch. If the result is wrong, revert the branch or use the IDE's local history rather than hand-editing dozens of files.
3. Verify before merging
Apply the replacement, then inspect the VCS diff hunk by hunk. Run the module's test suite. Finally, save the template and export it: a teammate can re-import the same template and re-run the search to confirm nothing was missed.
Trade-offs and limits
SSR is interactive and per-project. It cannot be wired into continuous integration (CI), it runs no tests of its own, and templates do not follow data flow across method boundaries. If a double value is passed through three helper methods before reaching the constructor, SSR will not trace that path. Semantic migrations that need type-flow analysis still belong in a tool like OpenRewrite. Scripted codemods—automated, version-controlled transformations—are the usual alternative when you need that depth.
Structural search ships in IntelliJ IDEA Community Edition, but language coverage depends on installed plugins, and non-JVM languages may behave differently. PSI matching also will not catch dynamically constructed code or reflection-based usages, so zero matches is not proof of a complete migration.
| Dimension | IntelliJ SSR | Scripted codemod |
|---|---|---|
| Review model | Interactive, per-hunk preview | Batch, gated by tests |
| Scope | Single project or module | Many repositories |
| CI integration | None | Native |
| Analysis depth | Syntactic | Semantic possible |
| Sharing | Exportable templates | Versioned scripts |
A practical decision rule
Reach for SSR when the pattern is syntactic, the scope is one repository, and you want human review of every change. Reach for a scripted codemod when the migration must repeat across many repos or be enforced by a test gate. Start today by exporting your BigDecimal template, committing it next to the module's README, and making the first migration on a scratch branch. The diff is your safety net; SSR just makes sure the diff contains code, not comments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.