- 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>]
# Mark Internal APIs in Analysis API Implementation Modules
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`](../../../compiler/psi/psi-api/testFixtures/org/jetbrains/kotlin/AbstractAnalysisApiInternalApiTest.kt)
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](/analysis/docs/contribution-guide/api-development.md#guard-api-endpoints-with-annotations)
for the full annotation guide and placement rules.
## Inputs
### Module selection (required)
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` |
| `analysis-api-standalone-fir` | `analysis/analysis-api-standalone/analysis-api-standalone-fir` | `:analysis:analysis-api-standalone:analysis-api-standalone-fir: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.
### Optional flags
| Input | Flag | Default | Description |
|---------------|---------------------|------------------------------------------|----------------------------------------------------------------------|
| IntelliJ repo | `--intellij=<path>` | `../ultimate` (relative to project root) | Path to the IntelliJ repository for additional usage search |
---
## Annotations Reference
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`, `analysis-api-standalone-fir` → always `@KaImplementationDetail`.
`analysis-api-standalone-fir` additionally exempts the `org.jetbrains.kotlin.idea.references` package through an
`isExempt` override — that package is a public surface so its declarations are never flagged and never reach this skill.
- `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`](../../../compiler/psi/psi-api/testFixtures/org/jetbrains/kotlin/AnalysisApiNonPublicMarkers.kt):
`@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.
---
## Phase 1: Run the codebase test in auto-apply mode
Replace any manual scan. The test discovers and annotates one violating file per run.
### Step 1: Invoke the test with auto-apply enabled
```bash
./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.
### Step 2: Interpret the outcome
- **Test passes (BUILD SUCCESSFUL):** every public declaration in `<module>/src/` is already marked correctly. Skip to
**Phase 5** to perform a final build check.
- **Test fails with `"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.
### Step 3: Identify the modified file
Use `git diff --name-only` (filtered to files under `<module>/src/`):
```bash
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.
---
## Phase 2: Diff, downgrade, upgrade
For the modified file from Phase 1.
### Step 1: Extract the newly added annotations
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:
- the **declaration** the annotation precedes (kind, name, FQN);
- the **inserted annotation** — `@KaImplementationDetail` or `@LLFirInternals`.
### Step 2: Classify each declaration's usages
For each newly marked declaration, determine where it is referenced **besides its own `src/`**:
- `<module>/tests/`, `<module>/testFixtures/`, `<module>/test/` count as **own tests**.
- Ignore `<module>/testData/`: those are data files, not Kotlin sources, and can mention internal class names without it
being a real usage.
- Anything else (other modules' `src/`/`tests/`, the IntelliJ repository) counts as **outside the module**.
Tools to run the search:
- **Within the Kotlin project:** use `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/**"]
```
- **Within the IntelliJ repository:** the IntelliJ repo is a separate project, so JetBrains MCP cannot search it. Use the
standard `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:**
- For declarations with common names (`create`, `get`, `resolve`), search for the qualified name or a distinctive usage
pattern (e.g., `ClassName.methodName`, `import ...ClassName`).
- When a hit is ambiguous, read the usage site to confirm it references this specific declaration.
- Classifier names (classes/interfaces/objects) are usually distinctive enough on their own.
### Step 3: Apply the per-declaration decision
| 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. |
View on GitHub