Skip to content

ADFA-2417: Add LSP support for Kotlin#907

Merged
Daniel-ADFA merged 12 commits intostagefrom
ADFA-2417-kotlin-lsp-experimental
Feb 13, 2026
Merged

ADFA-2417: Add LSP support for Kotlin#907
Daniel-ADFA merged 12 commits intostagefrom
ADFA-2417-kotlin-lsp-experimental

Conversation

@Daniel-ADFA
Copy link
Contributor

Add Kotlin LSP with semantic analysis, type inference, and stdlib indexing

Implement a full Kotlin Language Server built on tree-sitter parsing with:

  • Semantic analysis: type inference, overload resolution, symbol resolution
  • Code intelligence: completions, hover, go-to-definition, references,
    document symbols, signature help, semantic tokens, code actions, diagnostics
  • Symbol indexing: stdlib, classpath (JAR scanning), and project-level indexes
  • Stdlib index generation at build time via kotlin-stdlib-generator module

@Daniel-ADFA Daniel-ADFA marked this pull request as draft January 29, 2026 22:26
@Daniel-ADFA Daniel-ADFA requested review from a team, itsaky-adfa and jomen-adfa January 29, 2026 22:26
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a complete Kotlin LSP subsystem (parser, syntax/tree model, symbol model, indexing, semantic analysis, server and LSP providers), integrates it into the app/editor (diagnostics UI and clipboard copy), and updates build configuration and .gitignore for the new Kotlin LSP modules and generated stdlib index.

Changes

Cohort / File(s) Summary
Build & Ignore
gradle/libs.versions.toml, app/build.gradle.kts, editor/build.gradle.kts, lsp/kotlin-core/build.gradle.kts, .gitignore
Add lsp4j catalog entry, add projects.lsp.kotlin dependencies to app/editor, add kotlin-core module build script, and ignore generated lsp/kotlin/src/main/resources/stdlib-index.json.
App integrations & UI
app/src/main/java/.../IDELanguageClientImpl.java, app/src/main/java/.../DiagnosticsListFragment.kt, app/src/main/java/.../EditorBottomSheet.kt, app/src/main/java/.../strictmode/ViolationHandler.kt, app/src/main/res/layout/layout_editor_bottom_sheet.xml, app/src/main/java/.../utils/DiagnosticsFormatter.kt
Expose getAllDiagnostics(); load diagnostics on fragment resume; add copy-diagnostics FAB and clipboard wiring plus DiagnosticsFormatter; formatting-only changes in ViolationHandler.
Editor glue
editor/src/main/java/.../KotlinLanguage.kt, editor/src/main/java/.../EditorEventDispatcher.kt
Add Kotlin-specific editor behaviors (completion chars, symbol pairs, newline handlers) and change event dispatch to call document hooks synchronously before queueing.
Parser & tree model
lsp/kotlin-core/src/main/java/.../parser/*
Add Tree‑sitter Kotlin parser wrapper, SyntaxTree/SyntaxNode, Position/TextRange/PositionConverter, ParseResult/SourceEdit, SyntaxKind, and SyntaxVisitor framework.
Symbol model & types
lsp/kotlin-core/src/main/java/.../symbol/*
Introduce symbol hierarchy, TypeReference, Modifiers/Visibility, Scope/ScopeKind, SymbolLocation, SymbolTable, SymbolBuilder, and symbol visitor utilities.
Indexing & serialization
lsp/kotlin-core/src/main/java/.../index/*
Add symbol indexing and serializers: SymbolIndex/MutableSymbolIndex, FileIndex/ProjectIndex/PackageIndex/ClasspathIndex/StdlibIndex/ExtensionIndex, IndexEntry/IndexData, ClasspathIndexer, DependencyTracker, StdlibIndexLoader.
Semantic layer
lsp/kotlin-core/src/main/java/.../semantic/*
Add semantic stack: AnalysisContext, diagnostic model/collector, TypeInferrer, SymbolResolver, OverloadResolver, SemanticAnalyzer and related data types.
Server & runtime
lsp/kotlin-core/src/main/java/.../server/*
Add KotlinLanguageServer, KotlinTextDocumentService, KotlinWorkspaceService, AnalysisCache, AnalysisScheduler, DocumentManager, DocumentState, and workspace/classpath/stdlib management APIs.
LSP providers
lsp/kotlin-core/src/main/java/.../server/providers/*
Add LSP feature providers: CompletionProvider, HoverProvider, DefinitionProvider, DocumentSymbolProvider, DiagnosticProvider, SemanticTokenProvider, CodeActionProvider.
Large new files & heavy logic
lsp/kotlin-core/src/main/java/.../SymbolBuilder.kt, .../semantic/*, .../parser/*, .../index/*
Many large, complex implementations (parsing, symbol extraction, indexing, semantic analysis, providers) — substantial new code surface requiring deep review.

Sequence Diagram(s)

sequenceDiagram
    participant Editor as Editor (User)
    participant TDS as KotlinTextDocumentService
    participant Scheduler as AnalysisScheduler
    participant Parser as KotlinParser
    participant Builder as SymbolBuilder
    participant Analyzer as SemanticAnalyzer
    participant Index as ProjectIndex
    participant Client as LanguageClient

    Editor->>TDS: didOpen / didChange(uri, content)
    TDS->>Scheduler: scheduleAnalysis(uri)
    Scheduler->>Parser: parse(source)
    Parser-->>Scheduler: ParseResult (SyntaxTree)
    Scheduler->>Builder: build symbol table (SyntaxTree)
    Builder-->>Scheduler: SymbolTable
    Scheduler->>Analyzer: analyze(SymbolTable, SyntaxTree)
    Analyzer-->>Scheduler: Diagnostics, FileIndex
    Scheduler->>Index: update ProjectIndex with FileIndex
    Scheduler->>Client: publishDiagnostics(uri, diagnostics)
    Editor->>TDS: completion/hover/definition request
    TDS->>Index: query (ProjectIndex/AnalysisContext)
    Index-->>TDS: results
    TDS-->>Editor: LSP response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • jomen-adfa
  • itsaky-adfa

Poem

🐇 I hopped through trees of code so spry,

Parsed each leaf and watched it fly,
Indexed roots and mapped each part,
Now Kotlin blooms within our cart. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (53 files):

⚔️ .gitignore (content)
⚔️ app/build.gradle.kts (content)
⚔️ app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/app/strictmode/ViolationHandler.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/fragments/DiagnosticsListFragment.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/handlers/LspHandler.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java (content)
⚔️ app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/services/debug/DebuggerActionsOverlayAdapter.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt (content)
⚔️ app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt (content)
⚔️ app/src/main/res/layout/layout_editor_bottom_sheet.xml (content)
⚔️ build-info/build.gradle.kts (content)
⚔️ common/src/main/java/com/itsaky/androidide/syntax/colorschemes/DynamicColorScheme.kt (content)
⚔️ common/src/main/java/com/itsaky/androidide/syntax/colorschemes/SchemeAndroidIDE.java (content)
⚔️ composite-builds/build-logic/common/src/main/java/com/itsaky/androidide/build/config/BuildConfig.kt (content)
⚔️ composite-builds/build-logic/common/src/main/java/com/itsaky/androidide/build/config/CI.kt (content)
⚔️ composite-builds/build-logic/common/src/main/java/com/itsaky/androidide/build/config/ProjectConfig.kt (content)
⚔️ composite-builds/build-logic/plugins/build.gradle.kts (content)
⚔️ composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/SigningConfigPlugin.kt (content)
⚔️ composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/util/SigningKeyUtils.kt (content)
⚔️ editor/build.gradle.kts (content)
⚔️ editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt (content)
⚔️ editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorScheme.kt (content)
⚔️ editor/src/main/java/com/itsaky/androidide/editor/ui/EditorEventDispatcher.kt (content)
⚔️ flox/base/.flox/env/manifest.lock (content)
⚔️ flox/base/.flox/env/manifest.toml (content)
⚔️ flox/local/.flox/env/manifest.lock (content)
⚔️ gradle/libs.versions.toml (content)
⚔️ idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt (content)
⚔️ idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt (content)
⚔️ logsender/src/main/java/com/itsaky/androidide/logsender/LogSenderService.java (content)
⚔️ logsender/src/main/res/values/strings.xml (content)
⚔️ plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt (content)
⚔️ resources/src/main/res/drawable/fat_pen.png (content)
⚔️ resources/src/main/res/values/strings.xml (content)
⚔️ settings.gradle.kts (content)
⚔️ shared/src/main/java/com/itsaky/androidide/utils/DocumentUtils.java (content)
⚔️ subprojects/aapt2-proto/build.gradle.kts (content)
⚔️ subprojects/project-models/build.gradle.kts (content)
⚔️ subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AbstractModelBuilder.kt (content)
⚔️ termux/termux-app/src/main/java/com/itsaky/androidide/activities/TerminalActivity.kt (content)
⚔️ termux/termux-app/src/main/java/com/termux/app/TermuxActivity.java (content)
⚔️ termux/termux-app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java (content)
⚔️ termux/termux-emulator/src/main/java/com/termux/terminal/TerminalSession.java (content)
⚔️ termux/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java (content)

These conflicts must be resolved before merging into stage.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'ADFA-2417: Add LSP support for Kotlin' clearly summarizes the main change—adding Kotlin LSP support—which is the primary objective of the changeset.
Description check ✅ Passed The description is directly related to the changeset, detailing Kotlin LSP implementation with semantic analysis, code intelligence features, and symbol indexing—all reflected in the extensive file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ADFA-2417-kotlin-lsp-experimental

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 and usage tips.

@Daniel-ADFA Daniel-ADFA marked this pull request as ready for review January 29, 2026 22:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/IndexEntry.kt`:
- Around line 245-333: ClassEntry.companions are never emitted or counted:
update ClassEntry.toIndexedSymbols() to iterate companions and add IndexedSymbol
entries just like members (use name = member.name, fqName =
"$fqName.${member.name}", kind = parseKind(member.kind), packageName =
packageName, containingClass = fqName, signature = member.sig, parameters =
member.params.map { it.toIndexedParameter() }, returnType = member.ret,
visibility = parseVisibility(member.vis), deprecated = member.dep) and update
StdlibIndexData.totalCount to include companions (add it to the class member
count when iterating classes.values, e.g. count += it.members.size +
it.companions.size) so companion APIs are indexed and counted.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ProjectIndex.kt`:
- Around line 108-119: The current deduplication in getAllFunctions() using
distinctBy { it.fqName } collapses overloaded functions; update
getAllFunctions() (and similarly findBySimpleName(), findByPackage(),
findByPrefix(), getAllProperties(), getAllClasses(), findExtensions()) to avoid
losing overloads by either removing the fqName-only dedupe or change the dedupe
key to include signature/parameter info (e.g., use a composite key like
"${it.fqName}|${it.signature}" or a unique identifier field on IndexedSymbol);
ensure aggregation from fileIndexes, stdlibIndex, and classpathIndex still
merges sources but preserves distinct overload entries rather than collapsing by
fqName alone.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisScheduler.kt`:
- Around line 154-159: analyzeSync uses runBlocking(analysisDispatcher) which
deadlocks if called on the same single-threaded dispatcher; update analyzeSync
to detect when the current thread is the analysis thread and in that case call
performAnalysis(state) directly, otherwise continue to use
runBlocking(analysisDispatcher) { performAnalysis(state) }; reference
analysisDispatcher, analyzeSync, and performAnalysis and, if needed, capture the
analysis thread when creating analysisDispatcher (e.g. store Thread in a field)
to perform the thread equality check.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinLanguageServer.kt`:
- Around line 110-113: The loadStdlibIndex(InputStream) method currently leaves
the InputStream open; modify loadStdlibIndex to ensure the stream is closed
after loading (e.g., wrap the call to
StdlibIndexLoader.loadFromStream(inputStream) in a Kotlin use{} block or a
try-with-resources equivalent) so the InputStream is closed even on exceptions,
then set the result on projectIndex via
projectIndex.setStdlibIndex(stdlibIndex).
- Around line 229-235: The exit() method in KotlinLanguageServer currently calls
exitProcess(0/1) which will kill the embedding IDE; instead remove direct
exitProcess calls in KotlinLanguageServer.exit() and either invoke a provided
host callback (e.g., a nullable shutdownHandler or onExit lambda you add to the
class) or guard the exit with an "outOfProcess" flag so exitProcess is only
called for standalone deployments; update references to shutdownRequested and
ensure exit() calls the host-provided callback or sets a status/result for the
host to act on rather than calling exitProcess directly.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinTextDocumentService.kt`:
- Around line 62-75: The logs in KotlinTextDocumentService.kt currently print
raw document text for each change (in the loop over params.contentChanges and
the Log.d calls), which can leak user code and grow logs; replace those
Log.d(...) invocations to avoid logging change.text directly—log only safe
metadata such as change.text.length, a capped preview (e.g., first N chars) or a
hash, and keep context (index, range, version); update both the full-sync branch
where documentManager.update(...) is called and the incremental branch around
documentManager.applyChange(...) to use the sanitized message instead of the raw
text.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DefinitionProvider.kt`:
- Around line 118-131: The Location constructors are being passed plain
filesystem paths (ref.filePath / location.filePath) instead of URIs; update the
loop in DefinitionProvider (the block using projectIndex.findSymbolReferences
and creating Location) to call DocumentState.pathToUri(ref.filePath) when
constructing Location, and likewise fix the other occurrences by converting
paths to URIs in symbolToLocation and indexedSymbolToLocation methods in
DefinitionProvider and where KotlinWorkspaceService passes
location.filePath/filePath to Location so all Location(...) calls receive a URI
string.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt`:
- Around line 36-46: The literal branch in the hoverContent when-expression is
unreachable because node.kind.isKeyword matches literals first; update the when
order so literals are checked before keywords (i.e., move the
node.kind.isLiteral branch above node.kind.isKeyword) or alter the isKeyword
check to exclude literals; specifically modify the when in HoverProvider.kt
(hoverContent, node.kind) so getLiteralHover(node) can run before
getKeywordHover(node.kind) while keeping the SIMPLE_IDENTIFIER branch intact.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/ScopeKind.kt`:
- Around line 162-178: The canContainTypes and canContainProperties getters
currently omit ENUM, causing nested enum types/properties to be ignored; update
the boolean logic in the properties canContainTypes and canContainProperties to
include ENUM (same as isClassScope's treatment of ENUM) so enums are allowed to
contain nested type declarations and properties; locate the properties named
canContainTypes and canContainProperties in ScopeKind and add ENUM to their
membership checks.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt`:
- Around line 599-618: The render() currently appends the nullable marker after
a function type without parentheses, producing invalid Kotlin for nullable
function types; update render() so when functionTypeInfo != null and isNullable
is true you wrap the entire function type in parentheses before appending '?'
(e.g., produce "((...)->...)?" rather than "(...)->...?"); implement this by
emitting the opening parenthesis before rendering the function type and the
closing parenthesis after it when functionTypeInfo != null && isNullable, then
append '?' and keep the existing type-argument/nullability behavior for the
non-function branch unchanged.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolBuilder.kt`:
- Around line 48-58: The debug Log.d calls in SymbolBuilder.build (and other
methods in SymbolBuilder) unconditionally construct and emit source/S-expression
snippets which can leak user code and hurt performance; wrap these logging sites
with a guard that checks logging is enabled (e.g.,
android.util.Log.isLoggable("SymbolBuilder", android.util.Log.DEBUG) or a new
private helper method like shouldDebug() / debugLog { ... }) and only build the
debug string inside the guarded block or lambda so strings aren’t constructed
when debug is off; apply the same guarded-logging pattern to the other Log.d
calls in this class to avoid PII/perf risk.
🟡 Minor comments (29)
app/src/main/java/com/itsaky/androidide/utils/DiagnosticsFormatter.kt-14-14 (1)

14-14: Thread safety: SimpleDateFormat is not thread-safe.

SimpleDateFormat is shared at the object level and can produce incorrect results or throw exceptions when format() is called concurrently from multiple threads.

🛡️ Proposed fix: Create a new instance per call or use thread-safe alternative
 object DiagnosticsFormatter {

-    private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
+    private const val DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss"

     fun format(diagnostics: Map<File, List<DiagnosticItem>>): String {
         if (diagnostics.isEmpty()) return "No diagnostics"

         val builder = StringBuilder()
         builder.appendLine("=== Diagnostics Report ===")
-        builder.appendLine("Generated: ${dateFormat.format(Date())}")
+        builder.appendLine("Generated: ${SimpleDateFormat(DATE_FORMAT_PATTERN, Locale.US).format(Date())}")
app/src/main/res/layout/layout_editor_bottom_sheet.xml-101-112 (1)

101-112: Missing contentDescription for accessibility.

The FAB should have a contentDescription attribute to support screen readers. However, the proposed string resource @string/desc_copy_diagnostics does not exist and must be created. Also note that the other FABs in this file (clear_fab, share_output_fab) are also missing contentDescription attributes for consistency.

♿ Proposed fix: Add contentDescription
   <com.google.android.material.floatingactionbutton.FloatingActionButton
     android:id="@+id/copy_diagnostics_fab"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_above="@id/share_output_fab"
     android:layout_alignParentEnd="true"
     android:layout_marginStart="16dp"
     android:layout_marginTop="16dp"
     android:layout_marginEnd="16dp"
     android:layout_marginBottom="16dp"
     android:src="@drawable/ic_copy"
-    android:visibility="gone" />
+    android:visibility="gone"
+    android:contentDescription="@string/desc_copy_diagnostics" />

Add the string resource to values/strings.xml:

<string name="desc_copy_diagnostics">Copy diagnostics</string>
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ProjectIndex.kt-139-149 (1)

139-149: Race condition in updateFile between removal and addition to secondary indexes.

The sequence putremoveFromPackageIndex(old)addToPackageIndex(new) is not atomic. A concurrent read between removal and addition could miss the symbol. Consider using a lock or performing operations in a single synchronized block if strict consistency is required.

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DiagnosticProvider.kt-104-110 (1)

104-110: Adjacent ranges are treated as overlapping.

With half‑open ranges, end == start should be non‑overlapping. Current comparisons include adjacency, which can return diagnostics outside the requested range.

✅ Suggested fix (treat adjacency as non-overlap)
-        if (a.end.line == b.start.line && a.end.character < b.start.character) return false
-        if (a.start.line == b.end.line && a.start.character > b.end.character) return false
+        if (a.end.line == b.start.line && a.end.character <= b.start.character) return false
+        if (a.start.line == b.end.line && a.start.character >= b.end.character) return false
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolBuilder.kt-1684-1686 (1)

1684-1686: Swallowed exception hides parsing issues.

extractType catches Exception and returns null without logging, which makes failures hard to debug and triggers the detekt warning. At least log the exception (ideally via the guarded logger).

✅ Suggested fix (log the exception)
-        } catch (e: Exception) {
-            null
-        }
+        } catch (e: Exception) {
+            android.util.Log.w(logTag, "extractType failed for ${node.kind}", e)
+            null
+        }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/KotlinParser.kt-74-79 (1)

74-79: Wire the actual ABI version from TSLanguageKotlin or remove this property.

The languageVersion property documents tree-sitter language version for "debugging version mismatches," but tree-sitter only exposes an ABI version (not a Kotlin version). Returning a hardcoded 0 is misleading. Either retrieve the ABI version from the language bindings or drop the property if it's unused.

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/SymbolIndex.kt-289-289 (1)

289-289: Incorrect val/var distinction logic.

The condition returnType != null doesn't correctly distinguish between val and var properties—both have return types. The IndexedSymbol lacks an isVar field. Currently toSyntheticSymbol hardcodes isVar = false (line 365). Consider adding an isVar field to IndexedSymbol if mutable property tracking is needed.

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/PackageIndex.kt-68-71 (1)

68-71: Duplicate handling inconsistency between maps.

When add() is called with a symbol whose fqName already exists, fqNameMap overwrites the entry (correct), but packageMap appends a new entry (creates duplicate). This leads to inconsistent state where size (from fqNameMap) doesn't match actual symbol count in packageMap. The ClasspathIndex.addSymbol (lines 75-78) guards against this.

🛡️ Proposed fix to handle duplicates
 override fun add(symbol: IndexedSymbol) {
+    val existing = fqNameMap[symbol.fqName]
+    if (existing != null) {
+        packageMap[existing.packageName]?.remove(existing)
+    }
     fqNameMap[symbol.fqName] = symbol
     packageMap.getOrPut(symbol.packageName) { mutableListOf() }.add(symbol)
 }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxTree.kt-69-72 (1)

69-72: Documentation says "bytes" but implementation uses characters.

The sourceLength property is documented as "length of the source text in bytes" but source.length returns the character count. For ASCII-only sources these are equal, but for UTF-8 encoded sources with multi-byte characters, they differ. Tree-sitter typically operates on byte offsets. Clarify the intended semantics.

📝 Suggested documentation fix if character-based is intentional
     /**
-     * The length of the source text in bytes.
+     * The length of the source text in characters.
      */
     val sourceLength: Int get() = source.length

Or if byte-based is needed:

-    val sourceLength: Int get() = source.length
+    val sourceLength: Int get() = source.toByteArray(Charsets.UTF_8).size
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxTree.kt-129-150 (1)

129-150: Byte/character offset confusion in positionFromOffset.

The documentation refers to "byte offset" but the implementation treats the offset as a character index (iterating source by character). This discrepancy could cause incorrect position calculations for source files containing multi-byte UTF-8 characters (e.g., comments or strings with non-ASCII characters).

📝 Suggested documentation correction

If the intention is character-based offsets:

     /**
-     * Converts a byte offset to a [Position].
+     * Converts a character offset to a [Position].
      *
-     * `@param` byteOffset Byte offset in the source
+     * `@param` byteOffset Character offset in the source
      * `@return` Position (line and column)
      */
-    fun positionFromOffset(byteOffset: Int): Position {
+    fun positionFromOffset(offset: Int): Position {
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndexLoader.kt-182-190 (1)

182-190: Incorrect superTypes for Boolean and Char primitives.

All primitives are assigned superTypes = listOf("kotlin.Number", "kotlin.Comparable<...>"), but Boolean and Char do not extend kotlin.Number in Kotlin. This could lead to incorrect type inference or resolution results downstream.

🐛 Proposed fix
 private fun addPrimitiveTypes(index: StdlibIndex) {
-    val primitives = listOf(
+    val numericPrimitives = listOf(
         "Int" to "32-bit signed integer",
         "Long" to "64-bit signed integer",
         "Short" to "16-bit signed integer",
         "Byte" to "8-bit signed integer",
         "Float" to "32-bit floating point",
-        "Double" to "64-bit floating point",
-        "Boolean" to "Boolean value (true/false)",
-        "Char" to "16-bit Unicode character"
+        "Double" to "64-bit floating point"
     )

-    for ((name, _) in primitives) {
+    for ((name, _) in numericPrimitives) {
         index.addSymbol(IndexedSymbol(
             name = name,
             fqName = "kotlin.$name",
             kind = IndexedSymbolKind.CLASS,
             packageName = "kotlin",
             superTypes = listOf("kotlin.Number", "kotlin.Comparable<kotlin.$name>")
         ))
     }
+
+    // Boolean and Char don't extend Number
+    index.addSymbol(IndexedSymbol(
+        name = "Boolean",
+        fqName = "kotlin.Boolean",
+        kind = IndexedSymbolKind.CLASS,
+        packageName = "kotlin",
+        superTypes = listOf("kotlin.Comparable<kotlin.Boolean>")
+    ))
+
+    index.addSymbol(IndexedSymbol(
+        name = "Char",
+        fqName = "kotlin.Char",
+        kind = IndexedSymbolKind.CLASS,
+        packageName = "kotlin",
+        superTypes = listOf("kotlin.Comparable<kotlin.Char>")
+    ))
 }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisCache.kt-87-94 (1)

87-94: Potential race condition in eviction logic.

The evictIfNeeded method is not atomic: between checking cache.size >= maxCacheSize and finding/removing the oldest entry, concurrent modifications can occur. Additionally, minByOrNull iterates the entire map which is not guaranteed to be consistent under concurrent modification. Consider synchronizing or using a more robust concurrent eviction strategy.

🔒 Potential mitigation
 private fun <V : Timestamped> evictIfNeeded(cache: ConcurrentHashMap<String, V>) {
-    if (cache.size >= maxCacheSize) {
-        val oldestEntry = cache.entries
-            .minByOrNull { it.value.timestamp }
-            ?.key
-        oldestEntry?.let { cache.remove(it) }
+    // Best-effort eviction under concurrent access
+    while (cache.size >= maxCacheSize) {
+        val snapshot = cache.entries.toList()
+        val oldest = snapshot.minByOrNull { it.value.timestamp }?.key ?: break
+        if (cache.remove(oldest) != null) break
     }
 }

Alternatively, consider using a bounded cache implementation like Caffeine or Guava's Cache with built-in eviction policies.

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/FileIndex.kt-75-87 (1)

75-87: add() may create duplicate entries in symbolsByName.

If the same symbol is added twice (e.g., during re-indexing), symbolsByName will contain duplicates because getOrPut appends without checking if the symbol already exists in the list.

🛡️ Proposed fix to prevent duplicates
 override fun add(symbol: IndexedSymbol) {
+    // Remove existing entry if present to prevent duplicates
+    symbolsByFqName[symbol.fqName]?.let { existing ->
+        symbolsByName[existing.name]?.remove(existing)
+    }
     symbolsByFqName[symbol.fqName] = symbol
     symbolsByName.getOrPut(symbol.name) { mutableListOf() }.add(symbol)
 }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinWorkspaceService.kt-134-145 (1)

134-145: Potential concurrent modification when closing documents during folder removal.

Iterating documentManager.openUris while calling close(uri) inside the loop could cause ConcurrentModificationException if openUris returns a live collection view.

🛡️ Proposed fix to avoid concurrent modification
 private fun handleWorkspaceFolderRemoved(folder: WorkspaceFolder) {
     val folderUri = folder.uri
 
-    for (uri in documentManager.openUris) {
-        if (uri.startsWith(folderUri)) {
+    val urisToClose = documentManager.openUris.filter { it.startsWith(folderUri) }
+    for (uri in urisToClose) {
             documentManager.close(uri)
             projectIndex.removeFile(uri)
-        }
     }
 
     client?.logMessage(MessageParams(MessageType.Info, "Workspace folder removed: ${folder.uri}"))
 }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentState.kt-101-108 (1)

101-108: Debug logging may expose document content in production.

The verbose Log.d calls output deleted text and content near edits. While useful for debugging, consider guarding these with a debug flag or using a lower log level to avoid exposing document content in production logs.

🛡️ Suggested approach
+private const val DEBUG_SYNC = false  // Set to true for detailed sync debugging
+
 fun applyChange(...) {
     // ... offset calculations ...
 
-    Log.d(TAG, "[DOC-SYNC] applyChange: uri=$uri, range=$startLine:$startChar-$endLine:$endChar")
-    Log.d(TAG, "[DOC-SYNC]   offsets: start=$startOffset, end=$endOffset, contentLength=$oldLength")
-    Log.d(TAG, "[DOC-SYNC]   deleted='$deletedText', newText='$newText' (${newText.length} chars)")
+    if (DEBUG_SYNC) {
+        Log.d(TAG, "[DOC-SYNC] applyChange: uri=$uri, range=$startLine:$startChar-$endLine:$endChar")
+        Log.d(TAG, "[DOC-SYNC]   offsets: start=$startOffset, end=$endOffset, contentLength=$oldLength")
+        Log.d(TAG, "[DOC-SYNC]   deleted='$deletedText', newText='$newText' (${newText.length} chars)")
+    }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CompletionProvider.kt-669-704 (1)

669-704: Commas inside string literals may cause incorrect argument index calculation.

findCallContext counts commas to determine argumentIndex, but doesn't skip commas inside string literals. For example, println("a,b,c", x) would report argumentIndex=3 instead of 1.

🐛 Proposed fix to handle string literals
 private fun findCallContext(content: String, offset: Int): CallContext? {
     var parenDepth = 0
     var argIndex = 0
     var functionStart = -1
+    var inString = false
+    var stringChar: Char? = null
 
     for (i in (offset - 1) downTo 0) {
+        val c = content[i]
+        
+        // Track string boundaries (simplified - doesn't handle escapes perfectly)
+        if ((c == '"' || c == '\'') && (i == 0 || content[i - 1] != '\\')) {
+            if (inString && c == stringChar) {
+                inString = false
+                stringChar = null
+            } else if (!inString) {
+                inString = true
+                stringChar = c
+            }
+        }
+        
+        if (inString) continue
+        
-        when (content[i]) {
+        when (c) {
             ')' -> parenDepth++
             '(' -> {
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndexer.kt-63-65 (1)

63-65: Silently swallowed exceptions lose diagnostic context.

Per static analysis hints, the exceptions at lines 63-65 (JAR indexing) and 87-89 (AAR indexing) are swallowed without any logging. While skipping corrupted archives is reasonable, consider logging at debug level to aid troubleshooting when symbols are unexpectedly missing.

🔧 Proposed fix to add debug logging
         } catch (e: Exception) {
-            // Silently skip corrupted jars
+            // Skip corrupted jars but log for debugging
+            android.util.Log.d("ClasspathIndexer", "Failed to index JAR: ${jarFile.name}", e)
         }
         } catch (e: Exception) {
-            // Silently skip corrupted aars
+            // Skip corrupted AARs but log for debugging
+            android.util.Log.d("ClasspathIndexer", "Failed to index AAR: ${aarFile.name}", e)
         }

Also applies to: 87-89

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndexer.kt-527-537 (1)

527-537: Missing bounds checks in readU2 and readU4 could cause crashes.

These methods access bytes[offset] through bytes[offset + 3] without verifying the array bounds first. Malformed class files could cause ArrayIndexOutOfBoundsException that propagates up.

🛡️ Proposed defensive bounds check
     private fun readU2(bytes: ByteArray, offset: Int): Int {
+        if (offset + 1 >= bytes.size) return 0
         return ((bytes[offset].toInt() and 0xFF) shl 8) or
                 (bytes[offset + 1].toInt() and 0xFF)
     }
 
     private fun readU4(bytes: ByteArray, offset: Int): Int {
+        if (offset + 3 >= bytes.size) return 0
         return ((bytes[offset].toInt() and 0xFF) shl 24) or
                 ((bytes[offset + 1].toInt() and 0xFF) shl 16) or
                 ((bytes[offset + 2].toInt() and 0xFF) shl 8) or
                 (bytes[offset + 3].toInt() and 0xFF)
     }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndex.kt-194-207 (1)

194-207: Missing duplicate check in addSymbol — potential for duplicate entries.

Unlike ClasspathIndex.addSymbol (shown in relevant snippets at lines 74-86), this implementation doesn't check if a symbol with the same fqName already exists before adding. This could lead to duplicate entries in allSymbols if the same symbol is added multiple times.

🔧 Proposed fix to add duplicate check
     internal fun addSymbol(symbol: IndexedSymbol) {
+        if (symbolsByFqName.containsKey(symbol.fqName)) {
+            return
+        }
+
         allSymbols.add(symbol)
         symbolsByFqName[symbol.fqName] = symbol
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/TypeInferrer.kt-906-950 (1)

906-950: Dead code: inferCallableReference is never invoked.

The inferCallableReference method is fully implemented but is never called from inferTypeImpl. There's no SyntaxKind.CALLABLE_REFERENCE case in the dispatch. Either add the missing case or remove this dead code.

🔧 To enable callable reference inference, add to inferTypeImpl
             SyntaxKind.COLLECTION_LITERAL -> inferCollectionLiteral(node, scope, expectedType)
 
+            SyntaxKind.CALLABLE_REFERENCE -> inferCallableReference(node, scope)
+
             SyntaxKind.ASSIGNMENT -> ClassType.UNIT
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/ParseResult.kt-280-289 (1)

280-289: Multi-line insertions not handled in newEndPosition calculation.

The insertion factory calculates newEndPosition by adding insertedLength to the column, which is incorrect for multi-line text insertions (e.g., inserting "hello\nworld"). The new end position should account for line breaks in the inserted content.

🔧 Suggested approach

Consider accepting the actual newEndPosition as a parameter, or document that this helper is only for single-line insertions:

         fun insertion(position: Position, byteOffset: Int, insertedLength: Int): SourceEdit {
+            // Note: This assumes single-line insertion. For multi-line insertions,
+            // use the primary constructor with explicit newEndPosition.
             return SourceEdit(

Or refactor to accept the end position:

-        fun insertion(position: Position, byteOffset: Int, insertedLength: Int): SourceEdit {
+        fun insertion(position: Position, byteOffset: Int, insertedLength: Int, newEndPosition: Position? = null): SourceEdit {
             return SourceEdit(
                 startByte = byteOffset,
                 oldEndByte = byteOffset,
                 newEndByte = byteOffset + insertedLength,
                 startPosition = position,
                 oldEndPosition = position,
-                newEndPosition = position.copy(column = position.column + insertedLength)
+                newEndPosition = newEndPosition ?: position.copy(column = position.column + insertedLength)
             )
         }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CodeActionProvider.kt-165-194 (1)

165-194: Unused variable symbolTable and fragile val keyword detection.

  1. symbolTable is fetched on line 171 but never used — it can be removed.
  2. The findValDeclaration method uses indexOf("val ") which could match substrings in identifiers (e.g., interval , approval ). This could cause incorrect edits.
🔧 Proposed fixes
     private fun createChangeToVarAction(
         uri: String,
         diagnostic: Diagnostic,
         content: String
     ): List<CodeAction> {
-        val state = documentManager.get(uri) ?: return emptyList()
-        val symbolTable = state.symbolTable ?: return emptyList()
-
         val position = diagnostic.range.start
         val lines = content.lines()
         if (position.line >= lines.size) return emptyList()

For more robust val detection:

     private fun findValDeclaration(content: String, startLine: Int, startChar: Int): Pair<Int, Int>? {
         val lines = content.lines()
+        val valPattern = Regex("""(^|\s)val\s""")
 
         for (line in startLine downTo maxOf(0, startLine - 20)) {
             val lineContent = lines.getOrNull(line) ?: continue
-            val valIndex = lineContent.indexOf("val ")
-            if (valIndex >= 0) {
-                return line to valIndex
+            val match = valPattern.find(lineContent)
+            if (match != null) {
+                val valIndex = match.range.first + (if (match.groupValues[1].isNotEmpty()) 1 else 0)
+                return line to valIndex
             }
         }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SemanticAnalyzer.kt-143-146 (1)

143-146: Remove debug logging before release.

Multiple android.util.Log.d calls throughout this file (lines 143-146, 160-168, 343, 357, 364, 368, 392, 395, 447-459, 463, 466, 472) are Android-specific and inappropriate for an LSP core module. These should be removed or replaced with a cross-platform logging abstraction.

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentManager.kt-200-204 (1)

200-204: The character parameter is unused.

The getAtPosition method accepts a character parameter but never uses it. Either validate the character position against the line length, or remove the parameter if character-level validation is not needed.

Proposed fix (option 1: use the parameter)
     fun getAtPosition(uri: String, line: Int, character: Int): DocumentState? {
         val state = documents[uri] ?: return null
         if (line < 0 || line >= state.lineCount) return null
+        val lineLength = state.getLineLength(line)
+        if (character < 0 || character > lineLength) return null
         return state
     }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/SemanticTokenProvider.kt-330-356 (1)

330-356: Multiple force-unwraps (!!) on createToken results could cause NPE.

createToken returns null when the node text is empty (line 416). The force-unwraps at lines 341, 345, 349, 351-354 assume non-null but could crash if symbols have empty names.

Proposed fix
     private fun classifyFromSymbol(node: SyntaxNode, symbol: Symbol): SemanticToken? {
         return when (symbol) {
             is ClassSymbol -> {
                 val kind = when {
                     symbol.isInterface -> TYPE_INTERFACE
                     symbol.isEnum -> TYPE_ENUM
                     symbol.isObject -> TYPE_CLASS
                     symbol.isAnnotation -> TYPE_DECORATOR
                     else -> TYPE_CLASS
                 }
                 val mods = if (symbol.modifiers.isAbstract) MOD_ABSTRACT else 0
-                createToken(node, kind, mods)!!
+                createToken(node, kind, mods)
             }
             is FunctionSymbol -> {
                 val mods = buildFunctionModifiers(symbol)
-                createToken(node, TYPE_FUNCTION, mods)!!
+                createToken(node, TYPE_FUNCTION, mods)
             }
             is PropertySymbol -> {
                 val mods = if (!symbol.isVar) MOD_READONLY else 0
-                createToken(node, TYPE_PROPERTY, mods)!!
+                createToken(node, TYPE_PROPERTY, mods)
             }
-            is ParameterSymbol -> createToken(node, TYPE_PARAMETER, 0)!!
-            is TypeParameterSymbol -> createToken(node, TYPE_TYPE_PARAMETER, 0)!!
-            is TypeAliasSymbol -> createToken(node, TYPE_TYPE, 0)!!
-            else -> createToken(node, TYPE_VARIABLE, 0)!!
+            is ParameterSymbol -> createToken(node, TYPE_PARAMETER, 0)
+            is TypeParameterSymbol -> createToken(node, TYPE_TYPE_PARAMETER, 0)
+            is TypeAliasSymbol -> createToken(node, TYPE_TYPE, 0)
+            else -> createToken(node, TYPE_VARIABLE, 0)
         }
     }

Note: This changes the return type to SemanticToken?.

lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ExtensionIndex.kt-68-78 (1)

68-78: The remove method always returns true even if the extension wasn't found.

The method returns true unconditionally after line 77, regardless of whether the extension was actually present in allExtensions. This could mislead callers.

Proposed fix
     fun remove(extension: IndexedSymbol): Boolean {
         if (extension.receiverType == null) return false
 
-        allExtensions.remove(extension)
+        val removed = allExtensions.remove(extension)
+        if (!removed) return false
+        
         val normalizedReceiver = normalizeType(extension.receiverType)
         extensionsByReceiver[normalizedReceiver]?.remove(extension)
         if (extensionsByReceiver[normalizedReceiver]?.isEmpty() == true) {
             extensionsByReceiver.remove(normalizedReceiver)
         }
         return true
     }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolTable.kt-159-170 (1)

159-170: Remove debug logging statements before release.

The android.util.Log calls are Android-specific and will fail on non-Android JVMs. Since this is an LSP core module, it should be platform-agnostic. These debug logs should be removed or replaced with a platform-independent logging framework.

Proposed fix
     fun allVisibleSymbols(position: Position): List<Symbol> {
-        android.util.Log.d("SymbolTable", "allVisibleSymbols: position=$position")
-        android.util.Log.d("SymbolTable", "fileScope.range=${fileScope.range}, children=${fileScope.children.size}")
-        for (child in fileScope.children) {
-            android.util.Log.d("SymbolTable", "  child scope: kind=${child.kind}, range=${child.range}, symbols=${child.allSymbols.map { it.name }}")
-        }
         val scope = scopeAt(position) ?: fileScope
-        android.util.Log.d("SymbolTable", "found scope: kind=${scope.kind}, range=${scope.range}")
         val symbols = scope.collectAll { true }
-        android.util.Log.d("SymbolTable", "collectAll returned ${symbols.size} symbols: ${symbols.map { it.name }}")
         return symbols
     }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/OverloadResolver.kt-328-346 (1)

328-346: Add missing primitive array type handlers in resolveVarargElementType.

The method only handles kotlin.Array, kotlin.IntArray, and kotlin.LongArray. Kotlin also has DoubleArray, FloatArray, ByteArray, ShortArray, CharArray, and BooleanArray. When these types are not recognized, the method falls back to returning ClassType.ANY, causing loss of type information for vararg parameter compatibility checks.

Note: The codebase already handles all these array types in TypeInferrer (semantic type inference), so this gap in OverloadResolver creates inconsistency.

Proposed fix
     private fun resolveVarargElementType(
         param: ParameterSymbol,
         function: FunctionSymbol,
         scope: Scope?
     ): KotlinType {
         val arrayType = resolveParamType(param, function, scope)
         return when {
             arrayType is ClassType && arrayType.fqName == "kotlin.Array" -> {
                 arrayType.typeArguments.firstOrNull()?.type ?: ClassType.ANY
             }
             arrayType is ClassType && arrayType.fqName == "kotlin.IntArray" -> PrimitiveType.INT
             arrayType is ClassType && arrayType.fqName == "kotlin.LongArray" -> PrimitiveType.LONG
+            arrayType is ClassType && arrayType.fqName == "kotlin.DoubleArray" -> PrimitiveType.DOUBLE
+            arrayType is ClassType && arrayType.fqName == "kotlin.FloatArray" -> PrimitiveType.FLOAT
+            arrayType is ClassType && arrayType.fqName == "kotlin.ByteArray" -> PrimitiveType.BYTE
+            arrayType is ClassType && arrayType.fqName == "kotlin.ShortArray" -> PrimitiveType.SHORT
+            arrayType is ClassType && arrayType.fqName == "kotlin.CharArray" -> PrimitiveType.CHAR
+            arrayType is ClassType && arrayType.fqName == "kotlin.BooleanArray" -> PrimitiveType.BOOLEAN
             else -> ClassType.ANY
         }
     }
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt-471-477 (1)

471-477: Fix effectiveBound to match its documented contract.

The comment states "first bound or Any?", but the implementation returns null when no bounds exist. This contradicts Kotlin's type system semantics, where unbounded type parameters implicitly have Any? as their upper bound. A parallel implementation in KotlinType.kt already does this correctly.

Fix
-    val effectiveBound: TypeReference?
-        get() = bounds.firstOrNull()
+    val effectiveBound: TypeReference?
+        get() = bounds.firstOrNull() ?: ANY_NULLABLE

@itsaky-adfa
Copy link
Contributor

@Daniel-ADFA If this is still in progress, please mark it as a draft. If it's ready for review, please split the changes into multiple PRs so that we can review it.

…imental

# Conflicts:
#	.gitignore
#	resources/src/main/res/values/strings.xml
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt`:
- Around line 549-553: The clipboard copy sequence around
DiagnosticsFormatter.format(diagnostics) should be made defensive: obtain the
ClipboardManager with a safe cast (use
getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager) and check for
null before calling setPrimaryClip, and wrap setPrimaryClip in a try-catch that
catches SecurityException (and general Exception as a fallback); on failure call
the existing flashError/flashSuccess equivalent with a new error string resource
(e.g., msg_clipboard_copy_failed) instead of reusing msg_no_diagnostics_to_copy
so the user sees a distinct clipboard-failure message and the code won't crash.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt`:
- Around line 266-279: parseIntegerLiteral currently doesn't strip unsigned
suffixes so literals like 42u/42UL end up throwing NumberFormatException; update
parseIntegerLiteral to normalize and strip unsigned suffixes before parsing:
convert the literal to lowercase (or otherwise case-normalize), remove
underscores, then remove suffixes "ul" (or "lu" if you prefer to handle both
orders), "u" and "l" (in that order) so both "42u", "42UL", "42uL" are reduced
to the numeric digits, then proceed with the existing hex (startsWith("0x")) and
binary (startsWith("0b")) parsing paths and keep the same catch fallback;
reference parseIntegerLiteral and the cleaned variable when making the change.
- Around line 99-103: The lookup uses projectIndex.findByFqName(name) and a
limited findByPrefix fallback which both fail for simple identifiers; replace
that logic with a simple-name search by calling
projectIndex.findBySimpleName(name).firstOrNull() to populate indexedSymbol,
then return formatIndexedSymbolHover(indexedSymbol) as before; update the code
paths in HoverProvider (the indexedSymbol variable and the return that calls
formatIndexedSymbolHover) to use the findBySimpleName result.

In
`@lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt`:
- Around line 474-477: The KDoc for effectiveBound is misleading: it claims
"first bound or Any?" but the getter (effectiveBound) currently returns null
when bounds is empty; update the implementation to match the doc by returning a
TypeReference that represents Any? when bounds.firstOrNull() is null (i.e.,
return bounds.firstOrNull() ?: a TypeReference for Any?), or alternatively
update the KDoc to say "or null" if you prefer nullable behavior—locate the
effectiveBound getter in Symbol.kt and either create/return the existing project
representation of Any? as a TypeReference or change the KDoc to accurately state
that it may return null.
🧹 Nitpick comments (2)
lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt (2)

36-48: Previous literal/keyword ordering issue is resolved.

The isLiteral branch now correctly precedes the isKeyword branch, so literal hover is reachable. However, note that isKeyword (lines 324-327) still returns true for literal kinds like BOOLEAN_LITERAL and INTEGER_LITERAL because their enum names are all-uppercase with underscores. This is currently harmless due to the ordering, but it makes the when fragile — any future refactor that reorders or adds branches could reintroduce the bug.

Consider tightening isKeyword to explicitly exclude literals, or adding !isLiteral to the guard:

Suggested hardening
 private val SyntaxKind.isKeyword: Boolean
     get() = name.all { it.isUpperCase() || it == '_' } &&
             this != SyntaxKind.SIMPLE_IDENTIFIER &&
-            this != SyntaxKind.ERROR
+            this != SyntaxKind.ERROR &&
+            !isLiteral

64-83: Extract duplicate utility methods to shared module.

findNodeAtPosition and containsPosition are duplicated in both HoverProvider.kt and DefinitionProvider.kt with identical implementations. Consider extracting these into a shared utility (e.g., SyntaxNodeUtils.kt or extension functions) to prevent drift between the copies.

@Daniel-ADFA Daniel-ADFA merged commit b1292aa into stage Feb 13, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-2417-kotlin-lsp-experimental branch February 13, 2026 16:57
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.

2 participants