Skip to content

ADFA-4610: Fix Add-import action never appearing on unresolved references#1544

Closed
itsaky-adfa wants to merge 3 commits into
stagefrom
feat/ADFA-4610
Closed

ADFA-4610: Fix Add-import action never appearing on unresolved references#1544
itsaky-adfa wants to merge 3 commits into
stagefrom
feat/ADFA-4610

Conversation

@itsaky-adfa

@itsaky-adfa itsaky-adfa commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Jira: ADFA-4610

What & why

The "Add import" code action never appeared on an unresolved-reference diagnostic - dead since ADFA-3754. Root cause: KtSymbolIndex.findSymbolBySimpleName ended with Sequence.take(limit) and its only caller passed limit = 0, so take(0) returned empty and prepare() always went invisible.

This branch fixes that and hardens the action:

  1. Fix the dead action - treat limit <= 0 as unbounded in findSymbolBySimpleName, honoring the documented ReadableIndex.query contract the helper was violating. Extract computeImportCandidates (non-suspend, runCatching, KtFile fetched before project.read) for testability + crash-safety; drop the no-op CMD_FORMAT_CODE.
  2. No main-thread I/O in prepare() - prepare() runs synchronously on the UI thread during menu build and previously resolved against the SQLite index there. It now decides visibility from the in-memory KotlinDiagnosticExtra.unresolvedReference marker only (optimistic visibility); index resolution happens solely in the background execAction.
  3. Surface the empty case - since the action now shows for any unresolved reference, invoking it on one with no importable candidate reports "No imports found" (flashError).

Tests

:lsp:kotlin:testV7DebugUnitTest --tests AddImportActionTest 5/5 green (incl. the take(0)-unbounded regression guard); EditExtsTest covers sorted-insert/dedup; :lsp:kotlin:assembleV8Debug builds. Spotless can't run in the worktree (jgit ratchetFrom bug); formatting hand-verified.

Merge gate (manual QA)

On-device: "No imports found" flashbar on a typo; single-candidate auto-apply vs multi-candidate picker on a resolvable reference; no main-thread jank.

Follow-ups (out of scope)

  • insertImport does not dedup a concrete import already covered by a wildcard (no impact today - only concrete FQNs are inserted).
  • Uniform long-press/tooltip help across the Kotlin code actions.

…nces

AddImportAction.prepare() gated visibility on findSymbolBySimpleName(name, 0),
but the helper ended with Sequence.take(limit) and take(0) returns empty, so
the action was invisible for every unresolved-reference diagnostic - dead since
ADFA-3754. Treat limit <= 0 as unbounded, honoring the documented
ReadableIndex.query contract the helper was violating.

Also extract hasImportableClassifier and a non-suspend computeImportCandidates
(runCatching, KtFile fetched before project.read) for testability and
crash-safety, and drop the no-op CMD_FORMAT_CODE. Adds AddImportActionTest
(visibility-gate regression) and EditExtsTest.
Replace the SQLite-backed visibility gate with an in-memory check on the
unresolved-reference marker. prepare() runs synchronously on the UI thread
during menu build, so resolving against the symbol index there was
main-thread disk I/O; that resolution now happens only in the background
execAction.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@itsaky-adfa itsaky-adfa changed the title /newADFA-4610: Fix Add-import action never appearing on unresolved refere… ADFA-4610: Fix Add-import action never appearing on unresolved references Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Kotlin LSP add-import action now resolves classifier candidates into fully-qualified-name edit maps, handles unbounded symbol-index lookups, emits no-op commands, and includes coverage for candidate filtering and sorted import insertion.

Changes

Add-import flow

Layer / File(s) Summary
Unbounded symbol lookup contract
lsp/kotlin/.../compiler/index/KtSymbolIndex.kt
Non-positive lookup limits return all matching symbols, while positive limits remain capped.
Classifier candidate resolution
lsp/kotlin/.../actions/AddImportAction.kt
Import visibility and execution use classifier candidates keyed by fully-qualified name, with guarded failure handling and no-op commands.
Candidate and edit validation
lsp/kotlin/.../actions/AddImportActionTest.kt, lsp/kotlin/.../utils/EditExtsTest.kt
Tests cover candidate resolution, classifier filtering, lookup limits, sorted insertion, duplicate imports, and package-only files.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LSP
  participant AddImportAction
  participant KtSymbolIndex
  participant KtFile
  LSP->>AddImportAction: prepare unresolved reference
  AddImportAction->>KtSymbolIndex: find importable classifiers
  KtSymbolIndex-->>AddImportAction: matching symbols
  AddImportAction->>KtFile: compute import TextEdits
  KtFile-->>AddImportAction: edits keyed by fqName
  AddImportAction-->>LSP: CodeActionItems with no-op commands
Loading

Possibly related PRs

Suggested reviewers: jatezzz

Poem

I’m a rabbit, hopping imports in a row,
Sorting each name where the new lines go.
Classes emerge from the symbol-index stream,
Duplicate carrots vanish like a dream.
No-op commands keep the path light—
Clean Kotlin fixes, fluffy and bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: fixing the Add-import action so it appears for unresolved references.
Description check ✅ Passed The description accurately describes the regression, the fix, and the added tests, all of which match the changeset.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ADFA-4610

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt`:
- Around line 87-113: Replace the broad runCatching/getOrElse handling in
computeImportCandidates with a targeted catch for the specific expected
exception, such as IllegalArgumentException, while allowing unexpected
exceptions to propagate. Update the function KDoc to describe only the narrowed
recovery behavior and remove claims that all pipeline failures are converted to
an empty map.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c8aaad1-a7b5-46fe-9e97-adc2c3cf5a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 8077407 and 1fc89fc.

📒 Files selected for processing (4)
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportActionTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/EditExtsTest.kt

Comment on lines +87 to +113
/**
* Computes, for the unresolved [reference] in the file at [nioPath] within [env], a map from
* each importable classifier's fully-qualified name to the edits that add its import in sorted
* position. The [org.jetbrains.kotlin.psi.KtFile] is fetched BEFORE entering [read] (deadlock
* rule: never block on `getCurrentKtFile(...).get()` inside `project.read`). Keying by FQN
* collapses the duplicate a symbol picks up from being present in both the source and library
* indexes. Returns an empty map when there is nothing to import *and* whenever anything in this
* pipeline throws: the action framework only catches [IllegalArgumentException] and this runs on
* a coroutine scope with no exception handler, so an uncaught throw here would crash the app.
*/
internal fun computeImportCandidates(
env: AbstractCompilationEnvironment,
nioPath: Path,
reference: String,
): Map<String, List<TextEdit>> =
runCatching {
val ktFile = env.ktSymbolIndex.getCurrentKtFile(nioPath).get() ?: return emptyMap()
env.project.read {
env.ktSymbolIndex
.findSymbolBySimpleName(reference, limit = 0)
.filter { it.kind.isClassifier }
.associate { symbol -> symbol.fqName to insertImport(ktFile, symbol.fqName) }
}
}.getOrElse { e ->
logger.warn("Failed to compute import candidates for '{}'", reference, e)
emptyMap()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the broad catch-all with narrow exception handling.

The use of runCatching handles all Throwable instances, intentionally suppressing crashes for any unexpected errors. Based on learnings, prefer narrow exception handling that catches only the specific exception type reported in crashes (such as IllegalArgumentException) instead of a broad catch-all. This preserves fail-fast behavior during development. Uncaught exceptions should be allowed to crash the app so they can be exposed and addressed.

🛠️ Proposed fix to apply narrow exception handling and update KDoc
 	/**
 	 * Computes, for the unresolved [reference] in the file at [nioPath] within [env], a map from
 	 * each importable classifier's fully-qualified name to the edits that add its import in sorted
 	 * position. The [org.jetbrains.kotlin.psi.KtFile] is fetched BEFORE entering [read] (deadlock
 	 * rule: never block on `getCurrentKtFile(...).get()` inside `project.read`). Keying by FQN
 	 * collapses the duplicate a symbol picks up from being present in both the source and library
-	 * indexes. Returns an empty map when there is nothing to import *and* whenever anything in this
-	 * pipeline throws: the action framework only catches [IllegalArgumentException] and this runs on
-	 * a coroutine scope with no exception handler, so an uncaught throw here would crash the app.
+	 * indexes. Returns an empty map when there is nothing to import, or if an
+	 * [IllegalArgumentException] is thrown. Uncaught exceptions are intentionally allowed
+	 * to crash the app to enforce fail-fast behavior during development.
 	 */
 	internal fun computeImportCandidates(
 		env: AbstractCompilationEnvironment,
 		nioPath: Path,
 		reference: String,
 	): Map<String, List<TextEdit>> =
-		runCatching {
+		try {
 			val ktFile = env.ktSymbolIndex.getCurrentKtFile(nioPath).get() ?: return emptyMap()
 			env.project.read {
 				env.ktSymbolIndex
 					.findSymbolBySimpleName(reference, limit = 0)
 					.filter { it.kind.isClassifier }
 					.associate { symbol -> symbol.fqName to insertImport(ktFile, symbol.fqName) }
 			}
-		}.getOrElse { e ->
+		} catch (e: IllegalArgumentException) {
 			logger.warn("Failed to compute import candidates for '{}'", reference, e)
 			emptyMap()
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Computes, for the unresolved [reference] in the file at [nioPath] within [env], a map from
* each importable classifier's fully-qualified name to the edits that add its import in sorted
* position. The [org.jetbrains.kotlin.psi.KtFile] is fetched BEFORE entering [read] (deadlock
* rule: never block on `getCurrentKtFile(...).get()` inside `project.read`). Keying by FQN
* collapses the duplicate a symbol picks up from being present in both the source and library
* indexes. Returns an empty map when there is nothing to import *and* whenever anything in this
* pipeline throws: the action framework only catches [IllegalArgumentException] and this runs on
* a coroutine scope with no exception handler, so an uncaught throw here would crash the app.
*/
internal fun computeImportCandidates(
env: AbstractCompilationEnvironment,
nioPath: Path,
reference: String,
): Map<String, List<TextEdit>> =
runCatching {
val ktFile = env.ktSymbolIndex.getCurrentKtFile(nioPath).get() ?: return emptyMap()
env.project.read {
env.ktSymbolIndex
.findSymbolBySimpleName(reference, limit = 0)
.filter { it.kind.isClassifier }
.associate { symbol -> symbol.fqName to insertImport(ktFile, symbol.fqName) }
}
}.getOrElse { e ->
logger.warn("Failed to compute import candidates for '{}'", reference, e)
emptyMap()
}
/**
* Computes, for the unresolved [reference] in the file at [nioPath] within [env], a map from
* each importable classifier's fully-qualified name to the edits that add its import in sorted
* position. The [org.jetbrains.kotlin.psi.KtFile] is fetched BEFORE entering [read] (deadlock
* rule: never block on `getCurrentKtFile(...).get()` inside `project.read`). Keying by FQN
* collapses the duplicate a symbol picks up from being present in both the source and library
* indexes. Returns an empty map when there is nothing to import, or if an
* [IllegalArgumentException] is thrown. Uncaught exceptions are intentionally allowed
* to crash the app to enforce fail-fast behavior during development.
*/
internal fun computeImportCandidates(
env: AbstractCompilationEnvironment,
nioPath: Path,
reference: String,
): Map<String, List<TextEdit>> =
try {
val ktFile = env.ktSymbolIndex.getCurrentKtFile(nioPath).get() ?: return emptyMap()
env.project.read {
env.ktSymbolIndex
.findSymbolBySimpleName(reference, limit = 0)
.filter { it.kind.isClassifier }
.associate { symbol -> symbol.fqName to insertImport(ktFile, symbol.fqName) }
}
} catch (e: IllegalArgumentException) {
logger.warn("Failed to compute import candidates for '{}'", reference, e)
emptyMap()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt`
around lines 87 - 113, Replace the broad runCatching/getOrElse handling in
computeImportCandidates with a targeted catch for the specific expected
exception, such as IllegalArgumentException, while allowing unexpected
exceptions to propagate. Update the function KDoc to describe only the narrowed
recovery behavior and remove claims that all pipeline failures are converted to
an empty map.

Source: Learnings

@itsaky-adfa
itsaky-adfa requested a review from a team July 17, 2026 14:33
@itsaky-adfa itsaky-adfa self-assigned this Jul 17, 2026
@itsaky-adfa

Copy link
Copy Markdown
Contributor Author

Superseded by #1547. This PR was opened against ADFA-4610, but that subtask tracks the original feature (delivered via ADFA-3754). The same commits are re-attached to the correctly-scoped bug ADFA-4747 on branch feat/ADFA-4747 (#1547). Closing without merge; no work lost.

@itsaky-adfa
itsaky-adfa deleted the feat/ADFA-4610 branch July 17, 2026 14:49
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