with one click
analysis-api-mark-internal-apis
// Drive the per-module internal-API codebase test, then refine the suggested annotations down to `internal` (or up to `@KaImplementationDetail`) based on actual external usage
// Drive the per-module internal-API codebase test, then refine the suggested annotations down to `internal` (or up to `@KaImplementationDetail`) based on actual external usage
| name | analysis-api-mark-internal-apis |
| description | Drive the per-module internal-API codebase test, then refine the suggested annotations down to `internal` (or up to `@KaImplementationDetail`) based on actual external usage |
| user-invocable | true |
| disable-model-invocation | true |
| argument-hint | [--intellij=<path>] |
Public declarations in Analysis API implementation modules should not be exposed to users. They should be internal or
annotated with a visibility annotation. The codebase test
AbstractAnalysisApiInternalApiTest
already finds unmarked declarations and chooses a default annotation per module/package. This skill runs that test in
auto-apply mode, then refines each freshly marked declaration: downgrade to internal when it has no callers outside its
module, upgrade @LLFirInternals to @KaImplementationDetail when its callers reach outside analysis/low-level-api-fir/,
and keep the rest. Finally, fix any "internal exposed through public API" build errors by re-promoting internal declarations
to @KaImplementationDetail.
Reference: Read Guard API Endpoints with Annotations for the full annotation guide and placement rules.
Use AskUserQuestion to present a selection of modules. The user must pick one. Only modules that have a per-module
codebase test are listed here:
| Choice | Module path (project-relative) | Gradle codebase test task |
|---|---|---|
analysis-api-fir | analysis/analysis-api-fir | :analysis:analysis-api-fir:testCodebase |
analysis-api-impl-base | analysis/analysis-api-impl-base | :analysis:analysis-api-impl-base:testCodebase |
low-level-api-fir | analysis/low-level-api-fir | :analysis:low-level-api-fir:testCodebase |
Throughout this skill, <module> and <gradle-task> refer to the user's selection.
| Input | Flag | Default | Description |
|---|---|---|---|
| IntelliJ repo | --intellij=<path> | ../ultimate (relative to project root) | Path to the IntelliJ repository for additional usage search |
The codebase test owns the initial choice of marker for each unmarked declaration. The test's per-module logic lives in
the suggestedAnnotation overrides:
analysis-api-fir, analysis-api-impl-base → always @KaImplementationDetail.low-level-api-fir → @KaImplementationDetail for declarations in org.jetbrains.kotlin.analysis.low.level.api.fir.api
(and subpackages); @LLFirInternals for everything else.The set of annotations the test treats as already-marked (so they're never flagged) lives in
AnalysisApiNonPublicMarkers:
@KaImplementationDetail, @KaExperimentalApi, @KaPlatformInterface, @KaNonPublicApi, @KaIdeApi, @LLFirInternals.
The test also skips internal/private declarations, annotation class declarations annotated with @RequiresOptIn, and
declarations carrying override. None of those reach this skill.
The skill then refines the test's suggestion based on actual external usage. The rules differ slightly per module because of how sub-modules see each other:
In every implementation module, src/ and the test source sets are separate Kotlin modules — an internal declaration in src/ is
not visible from tests/ or testFixtures/. That's why "used in own tests" is enough to disqualify a declaration from being
internal. Only low-level-api-fir has its own opt-in marker (@LLFirInternals) for "test-accessible LL-FIR-internal" declarations;
the other modules don't have a similar marker because their implementations don't go that deep, and @KaImplementationDetail is the
catch-all for cross-sub-module visibility there.
| Test's suggestion | Where the declaration is referenced (besides own src/) | Action |
|---|---|---|
@KaImplementationDetail | Nowhere (only own src/) | Downgrade to internal |
@KaImplementationDetail | Anywhere else (own tests, outside the module, IntelliJ) | Keep |
@LLFirInternals | Nowhere (only own src/) | Downgrade to internal |
@LLFirInternals | Only inside analysis/low-level-api-fir/ (own tests/testFixtures, no usages outside) | Keep |
@LLFirInternals | Anywhere outside analysis/low-level-api-fir/ | Upgrade to @KaImplementationDetail |
The skill never adds annotations to declarations the codebase test didn't already touch — Phase 5 just builds and reports any remaining errors for manual review.
Replace any manual scan. The test discovers and annotates one violating file per run.
./gradlew <gradle-task> \
-Pkotlin.analysis.codebaseTest.internalApi.updateSourceCode=true \
-Pkotlin.test.instrumentation.disable.inputs.check=true
kotlin.analysis.codebaseTest.internalApi.updateSourceCode=true flips the abstract test into write-back mode: it writes
the suggested fix to the violating source file, then throws to stop iteration.kotlin.test.instrumentation.disable.inputs.check=true disables the test-input security manager so the test JVM is
allowed to write to source files.<module>/src/ is already marked correctly. Skip to
Phase 5 to perform a final build check."Auto-applied N marker annotation(s) to <File>.kt. ...": exactly one source file under
<module>/src/ was modified on disk. The next phases work on that file.If the test fails with a different message (compile error, security exception, etc.), stop and report the error to the user — the skill cannot make sense of unrelated failures.
Use git diff --name-only (filtered to files under <module>/src/):
git diff --name-only -- '<module>/src/'
If multiple files are reported, prefer the one the test message named (the message contains the file's basename). Other files in the diff are likely the user's pre-existing edits — don't touch them.
For the modified file from Phase 1.
The test only inserts lines of the form <indent>@<MarkerName> directly above a declaration. Use git diff <file> (or
mcp__idea__read_file on <file> and on the HEAD version via git show HEAD:<file>) to extract, for each insertion:
@KaImplementationDetail or @LLFirInternals.For each newly marked declaration, determine where it is referenced besides its own src/:
<module>/tests/, <module>/testFixtures/, <module>/test/ count as own tests.<module>/testData/: those are data files, not Kotlin sources, and can mention internal class names without it
being a real usage.src//tests/, the IntelliJ repository) counts as outside the module.Tools to run the search:
mcp__idea__search_text (preferred for exact names) or mcp__idea__search_regex,
with the paths parameter to scope the search. To distinguish own-tests from outside-the-module hits, run two queries
(or read each result's path). For example, for a declaration in analysis/low-level-api-fir/, an "outside" search is:
paths: ["!analysis/low-level-api-fir/**"]
…and an "own tests" search is:
paths: ["analysis/low-level-api-fir/tests/**", "analysis/low-level-api-fir/testFixtures/**"]
Grep tool against --intellij=<path>. Anything that hits in the IntelliJ repo counts as outside the module.
If the path doesn't exist, notify the user but don't abort — proceed with Kotlin-repo results only.Reducing false positives:
create, get, resolve), search for the qualified name or a distinctive usage
pattern (e.g., ClassName.methodName, import ...ClassName).| Test's suggestion | Where the declaration is referenced (besides own src/) | Action |
|---|---|---|
@KaImplementationDetail | Nowhere (only own src/) | Downgrade. Remove the inserted annotation line. Add internal before the declaration keyword. |
@KaImplementationDetail | Anywhere else (own tests, outside the module, IntelliJ) | Keep. No change. |
@LLFirInternals | Nowhere (only own src/) | Downgrade. Remove the inserted annotation line. Add internal before the declaration keyword. |
@LLFirInternals | Only inside analysis/low-level-api-fir/ (own tests/testFixtures, no usages outside) | Keep. No change. This is precisely what @LLFirInternals is for: making a src/ declaration accessible to LL FIR's own tests. |
@LLFirInternals | Anywhere outside analysis/low-level-api-fir/ | Upgrade. Replace the @LLFirInternals line with @KaImplementationDetail. Swap imports accordingly. |
Apply edits with mcp__idea__replace_text_in_file. Import housekeeping: when removing or replacing an annotation,
also remove the corresponding import if no other declaration in the file still uses it. When adding @KaImplementationDetail,
ensure import org.jetbrains.kotlin.analysis.api.KaImplementationDetail is present.
Companion-object exception. A companion object's marker must match its outer classifier's final marker. The companion
is reached through the outer class (Outer.member → Outer.Companion.member), so callers who can opt into the outer must
also reach the companion. Skip the usage analysis for companion objects in the diff entirely; instead, decide the outer
first using the table above, then sync the companion:
@KaImplementationDetail → companion stays @KaImplementationDetail.@LLFirInternals → companion stays @LLFirInternals.@KaImplementationDetail → upgrade the companion the same way (replace @LLFirInternals with
@KaImplementationDetail, swap imports).internal → drop the companion's annotation (Step 4 below).If the same diff includes a top-level classifier and nested classifiers within it, and the top-level was downgraded to
internal, the nested classifiers don't need their own marker — Kotlin's internal visibility transitively covers them.
For each nested classifier in the diff under that newly-internal parent: drop the inserted annotation (and any redundant
import), without adding internal.
The skill does not need to handle override declarations specially: the codebase test's isViolation filters them
out before they reach the diff (see AbstractAnalysisApiInternalApiTest.isViolation).
Run JetBrains MCP get_file_problems with errorsOnly=false on the modified file. Fix any warnings or errors related to
the changes (missing imports, opt-in requirements introduced by the new annotations, etc.).
Re-run Phase 1 Step 1 with the same flags. There are three outcomes:
Run a normal compilation of the module:
./gradlew :analysis:<module>:compileKotlin -q
If the build succeeds, proceed to Phase 6.
If the build fails, stop and present the errors verbatim to the user for manual review. The skill does not auto-fix build errors. Common failure modes the user will need to resolve:
internal is referenced
in the signature of a public declaration in the same module. The user typically resolves this by switching the exposed
declaration from internal to @KaImplementationDetail.OPT_IN_USAGE_ERROR at use sites — a callsite of a newly annotated declaration needs an @OptIn annotation, a
module-level optIn compiler option, or its own marker annotation. These can also be triggered by previously unannotated
declarations the test just marked.Auto-fixing any of these from the diff alone risks masking real problems (e.g., upgrading internal to
@KaImplementationDetail everywhere on exposure would defeat the point of distinguishing the two), so this is left for
manual review.
Present a per-classification breakdown:
internal (Phase 2 downgrades).@KaImplementationDetail (test's suggestion confirmed).@LLFirInternals (own-tests-only access; no usages outside analysis/low-level-api-fir/).@LLFirInternals to @KaImplementationDetail (Phase 2 upgrades).