Skip to content

ADFA-3588: Introduce plugin API binary compatibility tracking and version contract #1265

Open
Daniel-ADFA wants to merge 2 commits intostagefrom
feat/ADFA-3588
Open

ADFA-3588: Introduce plugin API binary compatibility tracking and version contract #1265
Daniel-ADFA wants to merge 2 commits intostagefrom
feat/ADFA-3588

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

Replaces the meaningless min_ide_version plugin compatibility check with a real plugin API versioning scheme backed by binary-compatibility-validator. The previous field could never enforce anything:

IDE version strings are timestamps (C-r-MMDD-HHMM), not comparable to the SemVer values plugins were declaring, and the field was only checked for non-blankness anyway.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Plugin API Binary Compatibility Tracking and Version Contract

Key Changes

  • Plugin API Versioning System: Introduced a new semantic versioning-based plugin API compatibility contract starting at version 1.0.0, replacing the previous ineffective min_ide_version scheme (which was incompatible with IDE version timestamps in format C-r-MMDD-HHMM)

  • Binary Compatibility Validator Integration: Added org.jetbrains.kotlinx.binary-compatibility-validator (v0.18.1) to track API surface changes and enforce compatibility baselines via plugin-api.api file

  • Comprehensive Plugin API Surface: Defined public plugin API including:

    • Core lifecycle (IPlugin, PluginContext, PluginLogger)
    • Metadata and permissions (PluginMetadata, PluginInfo, PluginPermission)
    • Extension interfaces for build actions, editor operations, code completion, UI contributions, project creation, and more
    • Service layer (IdeCommandService, IdeEditorService, IdeProjectService, etc.)
    • Template builder API (CgtTemplateBuilder)
  • API Compatibility Enforcement: Implemented PluginApiVersionChecker with semantic version parsing and compatibility validation:

    • Enforces matching major versions (incompatibilities throw PluginApiIncompatibleException)
    • Allows minor and patch version relaxation (IDE can support older plugin APIs)
    • Validates both required and current versions, defaulting to 1.0.0 when unspecified
  • Plugin Load Flow Changes: Extended PluginManager to validate API compatibility before loading, with specific exception handling for PluginApiIncompatibleException that releases reserved sidebar slots and logs compatibility errors

  • Manifest Schema Update: Replaced min_ide_version and max_ide_version with min_plugin_api_version in plugin manifests (AndroidManifest.xml metadata key: plugin.min_plugin_api_version)

  • Test Coverage: Added 19 test cases for PluginApiVersionChecker covering version parsing, compatibility scenarios, malformed inputs, and exception handling

Risk Considerations

  • Breaking Change: Plugins must update their manifests from plugin.min_ide_version to plugin.min_plugin_api_version; older plugins without the new field will default to 1.0.0, which may cause unexpected compatibility issues if they were designed for older APIs

  • Version String Defaults: Missing minPluginApiVersion defaults to "1.0.0" across multiple locations—ensure existing plugins in production have explicitly declared versions to avoid silent compatibility assumptions

  • Binary Compatibility Tracking: The plugin-api.api baseline file (1309 lines added) represents a snapshot of the current API surface; future changes must be validated against this baseline, and the baseline may require manual updates if intentional API breaks are needed

  • Internal API Annotation: InternalPluginApi enforces opt-in at compile time—plugins using annotated APIs will fail to compile, which is intentional but requires careful communication to plugin developers about stable vs. internal APIs

Walkthrough

This PR introduces a plugin API versioning system that replaces IDE version constraints with semantic versioning checks. It adds binary compatibility validation infrastructure, defines a comprehensive plugin API surface, implements plugin API version compatibility verification, and updates plugin metadata schema and manifest handling accordingly.

Changes

Cohort / File(s) Summary
Plugin Metadata Schema
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt
Replaces minIdeVersion/maxIdeVersion fields with minPluginApiVersion across plugin metadata, manifest parsing, and loader extraction logic.
API Validation Infrastructure
build.gradle.kts, gradle/libs.versions.toml
Adds binary-compatibility-validator Gradle plugin (v0.18.1) with apiValidation config restricting checks to plugin-api subproject.
Plugin API Surface Definition
plugin-api/api/plugin-api.api
Defines comprehensive public API surface including IPlugin lifecycle, PluginContext, extensions (build/editor/UI/documentation), services (IDE operations), and template builders.
Version Compatibility System
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/PluginApiVersion.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiIncompatibleException.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
Implements semantic versioning validation with major.minor.patch parsing, compatibility checks enforcing identical majors, and exception handling for version mismatches during plugin loading.
Internal API Annotation
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/InternalPluginApi.kt
Adds opt-in annotation with ERROR-level @RequiresOptIn to enforce compile-time visibility of internal APIs.
UI & Manifest Updates
app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt, templates-impl/src/main/java/com/itsaky/androidide/templates/impl/pluginProject/pluginManifest.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginSecurityManager.kt
Updates plugin details display to show minPluginApiVersion, generated manifests reference plugin.min_plugin_api_version, and relaxes validation to no longer require minIdeVersion.
Tests
plugin-manager/src/test/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionCheckerTest.kt
Comprehensive test suite covering semantic version parsing, compatibility logic, malformed version rejection, and exception behavior across 16 test cases.

Sequence Diagram(s)

sequenceDiagram
    participant PluginManager
    participant PluginLoader
    participant PluginApiVersionChecker
    participant IPlugin
    
    PluginManager->>PluginLoader: getPluginMetadata()
    PluginLoader-->>PluginManager: PluginManifest (with minPluginApiVersion)
    PluginManager->>PluginManager: Create PluginInfo from manifest
    PluginManager->>PluginApiVersionChecker: requireCompatible(pluginId, requiredVersion, currentVersion)
    alt Compatible
        PluginApiVersionChecker-->>PluginManager: Validation passes
        PluginManager->>IPlugin: activate/initialize
        IPlugin-->>PluginManager: Plugin loads successfully
    else Incompatible
        PluginApiVersionChecker-->>PluginManager: PluginApiIncompatibleException
        PluginManager->>PluginManager: Release sidebar slots
        PluginManager-->>PluginManager: Log compatibility error
        PluginManager->>PluginManager: Fail plugin load
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jomen-adfa

🐰 A versioning scheme hops into view,
Plugins now dance to API cues,
Compatibility checks stand tall,
Semantic semvers handle it all, 🎯
Integration's ready to thrive and bloom! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main objective of the PR: introducing plugin API binary compatibility tracking with a version contract to replace the ineffective min_ide_version check.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation for replacing min_ide_version with a proper plugin API versioning scheme using binary-compatibility-validator.
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.

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

✨ 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-3588

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt (1)

243-264: ⚠️ Potential issue | 🟠 Major

Don't silently promote missing min_plugin_api_version to 1.0.0.

This makes pre-contract plugins look compatible with the new API gate. Because this PR also changes the public plugin API surface, older plugins that haven't been rebuilt can still pass load-time validation and then fail later with linkage/runtime errors. Safer rollout options are: require the field explicitly, or map “missing” to a distinct legacy bucket that the loader blocks until those plugins are republished.

🛠️ Minimal first step
-            val pluginMinPluginApiVersion = metaData.getString("plugin.min_plugin_api_version") ?: "1.0.0"
+            val pluginMinPluginApiVersion = metaData.getString("plugin.min_plugin_api_version")

Then handle null explicitly at the load gate instead of treating it as a valid 1.0.0 declaration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`
around lines 243 - 264, The code currently coerces a missing
metaData.getString("plugin.min_plugin_api_version") into "1.0.0"; instead,
change pluginMinPluginApiVersion to preserve null (e.g., val
pluginMinPluginApiVersion = metaData.getString("plugin.min_plugin_api_version"))
and update the PluginManifest data class to accept a nullable
minPluginApiVersion (or a distinct legacy marker) so the loader can detect
"missing" explicitly; then enforce the load-time gate (in PluginLoader.load /
validation code) to reject or route null/legacy entries rather than treating
them as compatible with "1.0.0".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.kt`:
- Around line 42-48: The parse(raw: String) function can throw
NumberFormatException when converting oversized numeric segments with toInt();
wrap the Version(...) construction inside a try-catch that catches
NumberFormatException (and any other NumberFormat-related exceptions you
consider relevant) and return null on failure so malformed/overflowing inputs
map to null as expected by requireCompatible(); update/add a regression test to
feed an oversized segment like "999999999999.0.0" and assert parse returns null
(or that requireCompatible yields MALFORMED_VERSION).

---

Outside diff comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`:
- Around line 243-264: The code currently coerces a missing
metaData.getString("plugin.min_plugin_api_version") into "1.0.0"; instead,
change pluginMinPluginApiVersion to preserve null (e.g., val
pluginMinPluginApiVersion = metaData.getString("plugin.min_plugin_api_version"))
and update the PluginManifest data class to accept a nullable
minPluginApiVersion (or a distinct legacy marker) so the loader can detect
"missing" explicitly; then enforce the load-time gate (in PluginLoader.load /
validation code) to reject or route null/legacy entries rather than treating
them as compatible with "1.0.0".
🪄 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: ce3d0688-fe90-4873-93cd-f6c7e5f45251

📥 Commits

Reviewing files that changed from the base of the PR and between c9ed03b and 4c78cfa.

📒 Files selected for processing (15)
  • app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
  • build.gradle.kts
  • gradle/libs.versions.toml
  • plugin-api/api/plugin-api.api
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/InternalPluginApi.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/PluginApiVersion.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiIncompatibleException.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginSecurityManager.kt
  • plugin-manager/src/test/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionCheckerTest.kt
  • templates-impl/src/main/java/com/itsaky/androidide/templates/impl/pluginProject/pluginManifest.kt

Comment on lines +42 to +48
private fun parse(raw: String): Version? {
val match = SEMVER.matchEntire(raw.trim()) ?: return null
return Version(
major = match.groupValues[1].toInt(),
minor = match.groupValues[2].ifBlank { "0" }.toInt(),
patch = match.groupValues[3].ifBlank { "0" }.toInt(),
)
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.kt

Repository: appdevforall/CodeOnTheGo

Length of output: 2453


🏁 Script executed:

find . -type f -name "*PluginApiVersionChecker*Test*" -o -name "*Test*PluginApiVersionChecker*"

Repository: appdevforall/CodeOnTheGo

Length of output: 179


🏁 Script executed:

rg -l "PluginApiVersionChecker" --type kotlin

Repository: appdevforall/CodeOnTheGo

Length of output: 373


🏁 Script executed:

cat -n ./plugin-manager/src/test/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionCheckerTest.kt

Repository: appdevforall/CodeOnTheGo

Length of output: 5711


Catch NumberFormatException in parse() to properly handle oversized numeric segments.

Input like 999999999999.0.0 matches the SEMVER regex but causes toInt() to throw NumberFormatException on overflow, escaping as an uncaught exception instead of returning null. This breaks the intended contract where requireCompatible() expects null to signal MALFORMED_VERSION. Wrap the Version construction in a try-catch block to return null on parse failure, and add a regression test for numeric overflow.

🔧 Targeted fix
     private fun parse(raw: String): Version? {
         val match = SEMVER.matchEntire(raw.trim()) ?: return null
-        return Version(
-            major = match.groupValues[1].toInt(),
-            minor = match.groupValues[2].ifBlank { "0" }.toInt(),
-            patch = match.groupValues[3].ifBlank { "0" }.toInt(),
-        )
+        return try {
+            Version(
+                major = match.groupValues[1].toInt(),
+                minor = match.groupValues[2].ifBlank { "0" }.toInt(),
+                patch = match.groupValues[3].ifBlank { "0" }.toInt(),
+            )
+        } catch (_: NumberFormatException) {
+            null
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.kt`
around lines 42 - 48, The parse(raw: String) function can throw
NumberFormatException when converting oversized numeric segments with toInt();
wrap the Version(...) construction inside a try-catch that catches
NumberFormatException (and any other NumberFormat-related exceptions you
consider relevant) and return null on failure so malformed/overflowing inputs
map to null as expected by requireCompatible(); update/add a regression test to
feed an oversized segment like "999999999999.0.0" and assert parse returns null
(or that requireCompatible yields MALFORMED_VERSION).

Comment on lines +45 to +47
major = match.groupValues[1].toInt(),
minor = match.groupValues[2].ifBlank { "0" }.toInt(),
patch = match.groupValues[3].ifBlank { "0" }.toInt(),
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.

toInt might throw in case of invalid input. Please add safeguards here (and test cases).

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