ADFA-3588: Introduce plugin API binary compatibility tracking and version contract #1265
ADFA-3588: Introduce plugin API binary compatibility tracking and version contract #1265Daniel-ADFA wants to merge 2 commits intostagefrom
Conversation
📝 WalkthroughPlugin API Binary Compatibility Tracking and Version ContractKey Changes
Risk Considerations
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
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 | 🟠 MajorDon't silently promote missing
min_plugin_api_versionto1.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
nullexplicitly at the load gate instead of treating it as a valid1.0.0declaration.🤖 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
📒 Files selected for processing (15)
app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.ktbuild.gradle.ktsgradle/libs.versions.tomlplugin-api/api/plugin-api.apiplugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.ktplugin-api/src/main/kotlin/com/itsaky/androidide/plugins/InternalPluginApi.ktplugin-api/src/main/kotlin/com/itsaky/androidide/plugins/PluginApiVersion.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiIncompatibleException.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginSecurityManager.ktplugin-manager/src/test/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionCheckerTest.kttemplates-impl/src/main/java/com/itsaky/androidide/templates/impl/pluginProject/pluginManifest.kt
| 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(), | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionChecker.ktRepository: 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 kotlinRepository: appdevforall/CodeOnTheGo
Length of output: 373
🏁 Script executed:
cat -n ./plugin-manager/src/test/kotlin/com/itsaky/androidide/plugins/manager/security/PluginApiVersionCheckerTest.ktRepository: 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).
| major = match.groupValues[1].toInt(), | ||
| minor = match.groupValues[2].ifBlank { "0" }.toInt(), | ||
| patch = match.groupValues[3].ifBlank { "0" }.toInt(), |
There was a problem hiding this comment.
toInt might throw in case of invalid input. Please add safeguards here (and test cases).
Replaces the meaningless
min_ide_versionplugin compatibility check with a real plugin API versioning scheme backed bybinary-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.