How does the CodeGenerator emit a new Kotlin source file, and what is the purpose of the Dependencies argument it requires?
answer
- createNewFile(dependencies, pkg, name, ext='kt') -> OutputStream
- Dependencies = incremental input->output map
- aggregating=false isolating, true = summary over all
- containingFile gives the source KSFile
- FileAlreadyExists if same name twice
basics
~10 sCall codeGenerator.createNewFile(...) to get an output stream and write Kotlin code into it. The Dependencies argument tells KSP which input files the generated file came from, so incremental builds know when to regenerate it.
solid answer
~40 sThe CodeGenerator writes generated sources that are added to the compilation. You call createNewFile(dependencies: Dependencies, packageName: String, fileName: String, extensionName: String = "kt"): OutputStream, then write text (often via OutputStreamWriter). The Dependencies argument is the heart of KSP's incremental support: it records which source files the generated file was derived from, so if those inputs change, KSP knows to delete and regenerate this output, and if they don't, it can be skipped. Construct it as Dependencies(aggregating: Boolean, vararg sources: KSFile). aggregating=false means the output depends only on the listed files; aggregating=true means it also depends on any new files appearing in later rounds (used when output is a summary over all matches). Use Dependencies.ALL_FILES sparingly — it forces regeneration on any input change. Get the originating KSFile via declaration.containingFile.
code
kotlin · 19 lines// Aggregating example: one registry summarizing ALL annotated types
val annotated = resolver.getSymbolsWithAnnotation("com.example.Component")
.filterIsInstance<KSClassDeclaration>()
.filter { it.validate() }
.toList()
val sourceFiles = annotated.mapNotNull { it.containingFile }.toTypedArray()
codeGenerator.createNewFile(
Dependencies(aggregating = true, *sourceFiles),
"com.example.generated",
"ComponentRegistry",
).use { out ->
val body = buildString {
appendLine("package com.example.generated")
appendLine("val components = listOf(")
annotated.forEach { appendLine(" \"${it.qualifiedName?.asString()}\",") }
appendLine(")")
}
out.write(body.toByteArray())
}go deeper
Knows CodeGenerator.createNewFile writes a generated source file and that you write code into the returned stream.
Knows the createNewFile signature, uses containingFile, and writes valid Kotlin (or uses KotlinPoet).
Explains Dependencies' role in incremental builds and the isolating vs aggregating distinction with correct examples.
Reasons about incremental correctness across multi-round/multimodule builds, idempotence to avoid duplicate-file exceptions, and the build-time cost tradeoffs of dependency granularity.
## Emitting a file The **`CodeGenerator`** (from the environment) is how a processor outputs sources. The primary call: ```kotlin val file: OutputStream = codeGenerator.createNewFile( dependencies = Dependencies(aggregating = false, sourceClass.containingFile!!), packageName = "com.example.generated", fileName = "PersonBuilder", extensionName = "kt", // default is "kt"; can be "java" or others ) OutputStreamWriter(file, Charsets.UTF_8).use { w -> w.write("package com.example.generated\n\n") w.write("class PersonBuilder { /* ... */ }\n") } ``` - `createNewFile` returns a raw `OutputStream`; you write **text** into it. Many processors use **KotlinPoet** (`FileSpec.writeTo(codeGenerator, dependencies)`) instead of hand-writing strings, but the underlying contract is the same. - The generated file's location on disk is managed by KSP; the path is `packageName` + `fileName` + `extensionName`. KSP automatically **adds the directory to the compilation**, so the generated code is compiled in the same build. - `createNewFileByPath(...)` exists for non-source resources. ## Why Dependencies matters: incremental compilation KSP supports **incremental processing** — on a rebuild it only reprocesses what changed. To do that correctly it must know the **input→output mapping**: which source files each generated file was produced from. That is exactly what the **`Dependencies`** object encodes. ```kotlin class Dependencies(aggregating: Boolean, vararg sources: KSFile) ``` - **`sources`** — the originating `KSFile`s. Obtain one with `someDeclaration.containingFile` (nullable — declarations from the classpath have no source file). - **`aggregating`**: - **`false`** (isolating) — the output depends **only** on the listed source files. If none of them changed, the output is kept; if one changed, only this output is regenerated. Use this when each input maps to its own output (e.g. one builder per annotated class). - **`true`** (aggregating) — the output also depends on **any newly added/removed file** in later rounds. Use this when the output is a *summary* across many inputs (e.g. a single registry listing all annotated types), because adding a new annotated class must regenerate that registry. - **`Dependencies.ALL_FILES`** — a convenience meaning "depends on every source file." Correct but pessimistic: any change regenerates the output. Avoid unless truly necessary. Getting Dependencies wrong doesn't break a clean build, but it breaks **incremental** builds — either stale output (too few deps) or constant regeneration (too many). ## Idempotence and rounds Never call `createNewFile` twice for the **same** package+name in one processing run — KSP throws `FileAlreadyExistsException`. Because newly generated files can trigger another round, guard generation (e.g., track what you've already produced) so you don't re-emit. ## Worked example with isolating dependencies ```kotlin resolver.getSymbolsWithAnnotation("com.example.Builder") .filterIsInstance<KSClassDeclaration>() .filter { it.validate() } .forEach { decl -> val src = decl.containingFile ?: return@forEach codeGenerator.createNewFile( Dependencies(aggregating = false, src), decl.packageName.asString(), "${decl.simpleName.asString()}Builder", ).use { out -> out.write("// generated\n".toByteArray()) } } ``` ## Key terms - **CodeGenerator** — API that writes generated sources/resources into the build. - **Dependencies** — input→output mapping powering incremental builds. - **aggregating** — whether the output depends on the set of all inputs vs. specific ones. - **KSFile** — a source file symbol; obtained via `containingFile`.
- When should aggregating be true rather than false?When the generated file summarizes information across many inputs (e.g. a single registry/index of all annotated classes), so that adding or removing an annotated class in a later round regenerates the summary. Per-input outputs use aggregating=false (isolating).
- What happens if you pass the wrong Dependencies?Clean builds still work, but incremental builds break: too few dependencies yields stale generated code that isn't regenerated when its input changes; too many (e.g. ALL_FILES everywhere) causes unnecessary regeneration and slow builds.
Dependencies is like listing the ingredients on a recipe card: if an ingredient changes you re-cook that dish; aggregating=true means 'also re-cook if any new ingredient is added to the pantry.'
saying these in an interview costs you the question
- Treating Dependencies as optional metadata with no effect
- Using Dependencies.ALL_FILES everywhere out of caution
- Forgetting containingFile can be null for classpath declarations
- Calling createNewFile twice for the same package+name in one run
- Confusing aggregating semantics (per-input vs summary-over-all)