codegen
Documents the Fakt code generation pipeline — immutable AST model, builder DSL, renderer, default value strategies, and extension functions. Use when modifying code generation, changing generated outp
SKILL.md.claude/skills/codegen/SKILL.md
GitHubCodegen Pipeline Guide
The codegen system transforms the shared FakeDeclaration model into Kotlin source files through a layered pipeline: Model (immutable AST) → Builder (DSL construction) → Renderer (string output).
Instructions
1. Understand the Pipeline
FakeDeclaration (from FIR; legacy IR also builds it)
↓
SpecToCodegenMapper (toCodegenSpecs → method/property specs)
↓
FakeGenerator / ConfigurationDslGenerator (orchestrators)
↓
codeFile("pkg") { klass("...") { ... } } (Builder DSL)
↓
CodeFile (immutable AST) (Model)
↓
CodeFile.renderTo(CodeBuilder) (Renderer)
↓
Kotlin source string (written to test source set)
All codegen files live under codegen-runtime/src/main/kotlin/com/rsicarelli/fakt/codegen/.
2. Model Layer — codegen/model/CodeFile.kt
Single file, fully immutable value objects. Key types:
| Type | Purpose |
|---|---|
CodeFile |
Root: package + imports + declarations |
CodeDeclaration |
Sealed: CodeClass, CodeDataClass, CodeFunction, CodeProperty |
CodeMember |
Sealed: class body items (CodeFunction, CodeProperty, CodeComment, CodeRegionStart/End) |
CodeClass |
Class with modifiers, supertypes, members, constructor properties |
CodeDataClass |
Data class with typed properties |
CodeFunction |
Function with params, return type, body |
CodeProperty |
Property with initializer, getter/setter |
CodeType |
Sealed: Simple, Generic, Nullable, Lambda |
CodeBlock |
Sealed: Expression, Statements, Empty |
CodeExpression |
Sealed: StringLiteral, NumberLiteral, Lambda, FunctionCall, Raw |
CodeModifier |
Enum: PUBLIC, PRIVATE, INTERNAL, OVERRIDE, SUSPEND, INLINE, etc. |
ConstructorProperty |
Constructor parameter with optional default value |
3. Builder Layer — codegen/builder/
Entry point: codeFile(packageName) { } — returns CodeFile. All builders use @CodeDsl marker.
CodeFileBuilder (top-level):
codeFile("com.example") {
header = "Generated by Fakt"
import("kotlinx.coroutines.flow.StateFlow")
klass("FakeImpl") { /* ClassBuilder */ }
function("fakeFactory") { /* FunctionBuilder */ }
dataClass("CallData") { /* DataClassBuilder */ }
}
ClassBuilder — klass(name) { }:
klass("FakeUserServiceImpl") {
public(); internal()
typeParam("T", "Comparable<T>")
implements("UserService") // Simple supertype
implements("Repository", "User") // Generic supertype
annotation("OptIn", "ExperimentalApi::class")
constructorProperty("getBehavior", "(String) -> User?") { private(); defaultValue = "null" }
region("Properties")
property("count", "Int") { private(); mutable(); initializer = "0" }
function("getUser") { override(); returns("User?"); expressionBody = "getBehavior(id)" }
endRegion()
}
FunctionBuilder — function(name) { }:
function("getUser") {
override(); suspend(); inline(); operator()
receiver("MyType")
typeParam("T", "Comparable<T>", reified = true)
parameter("id", "String")
parameter("count", "Int", defaultValue = "0")
parameter("items", "String", vararg = true)
returns("User?")
compact = true // no blank line before next compact sibling
body = "return getBehavior(id)" // block body (multi-line: split on \n)
expressionBody = "getBehavior(id)" // = syntax (mutually exclusive with body)
body { buildString { /* lambda */ } } // builder pattern
}
PropertyBuilder — property(name, type) { }:
property("name", "String") {
override(); private(); mutable()
compact = true
initializer = "\"default\""
getter = "field.uppercase()" // get() = ... or get() { ... } if multiline
setter = "field = value"
}
DataClassBuilder — dataClass(name) { }:
dataClass("GetUserCall") {
public()
property("id", "String")
property("includeDeleted", "Boolean")
}
4. Renderer Layer — codegen/renderer/
CodeBuilder — StringBuilder wrapper with indentation:
val cb = CodeBuilder(indentSize = 4)
cb.appendLine("val x = 1") // auto-indented
cb.indent { appendLine("inner") } // increments/decrements level
cb.block("class Foo") { ... } // "class Foo {" + indented body + "}"
cb.build() // final string
Rendering.kt — extension functions: CodeFile.renderTo(builder), CodeFile.renderToString(), and render functions for each model type. Key behaviors:
Unitreturn type omitted on expression bodies- KDoc rendered before annotations
- Compact siblings skip blank lines between them
- Multi-line constructor properties when > 1
- Package segments with Kotlin keywords get backtick-escaped
5. Strategy Layer — codegen/strategy/
DefaultValueResolver — composes strategies in priority order:
| Priority | Strategy | Handles |
|---|---|---|
| 1 | NullableDefaultStrategy |
T? → null |
| 2 | PrimitiveDefaultStrategy |
String → "", Int → 0, Boolean → false, etc. |
| 3 | StdlibDefaultStrategy |
Unit, Flow<T> → emptyFlow(), StateFlow<T> → MutableStateFlow(...), Result<T> |
| 4 | CollectionDefaultStrategy |
List<T> → emptyList(), Map<K,V> → emptyMap(), Array<T>, Mutable* |
| fallback | error | "Type '...' requires explicit configuration..." |
To add a new default strategy: implement DefaultValueStrategy interface (supports() + defaultValue()), add to the list in DefaultValueResolver.
6. Extensions Layer — codegen/extensions/
Spec types bridging the FakeDeclaration model to builders:
| Spec | Purpose |
|---|---|
MethodSpec |
Method metadata: name, params, return type, suspend, operator, default behavior |
PropertySpec |
Property metadata: name, type, isStateFlow, isMutable, default behavior |
AnnotationSpec |
Annotation with OptIn detection |
FactoryFunctionSpec |
Factory function metadata for fakeXxx {} generation |
Extension functions on builders (in separate files):
| File | Functions | Purpose |
|---|---|---|
FakeGenerator.kt |
generateCompleteFake() |
Main fake class generation |
MethodExtensions.kt |
overrideMethod(), overrideVarargMethod(), configureMethod() |
Method overrides + configure methods |
PropertyExtensions.kt |
stateFlowProperty(), behaviorProperty(), suspendBehaviorProperty(), nullableBehaviorProperty(), nullableSuspendBehaviorProperty() |
Property overrides + behavior lambdas |
CallTrackingExtensions.kt |
callHistoryBackingField(), propertyGetterCallHistoryField(), propertySetterCallHistoryField() |
Call tracking fields |
CallHistoryDsl.kt |
callDataClass(), verifierClass(), unitVerifierClass(), verifyFunction(), unitVerifyFunction() |
Call history data classes + verifiers |
CallHistoryGenerator.kt |
addCallHistoryComponents(), generateCallHistoryDeclarations() |
Orchestrates all call history code |
FactoryExtensions.kt |
generateFactoryFunctionCodeFile() |
fakeXxx {} factory function |
TypeParamRegex.kt |
eraseTypeParamsToAny(), typeContainsAnyParam() |
Generic type erasure utilities |
HistoryStorageInfo.kt |
resolveHistoryStorageType() |
Resolves call history storage (unit vs data class) |
KDocGenerator.kt |
KDoc generation utilities | Generated documentation for fakes |
TypeParamsInfo.kt |
Type parameter analysis helpers | Type parameter metadata extraction |
VisibilityExtensions.kt |
Visibility mapping helpers | FIR visibility to codegen modifier mapping |
7. Generation Orchestrators — codegen/generator/ (mapper in codegen/analysis/)
| File | Class | Role |
|---|---|---|
analysis/SpecToCodegenMapper.kt |
— | FakeDeclaration.toCodegenSpecs(), toRenderableMethodSpec(), toRenderablePropertySpec() |
generator/ImplementationGenerator.kt |
ImplementationGenerator |
Produces FakeXxxImpl class + factory function + call history |
generator/ConfigurationDslGenerator.kt |
ConfigurationDslGenerator |
Produces FakeXxxConfig DSL class |
ImplementationGenerator.generateImplementation() flow:
decl.toCodegenSpecs()→ method and property specsgenerateCompleteFake()→CodeFile(impl class)generateFactoryFunctionCodeFile()→CodeFile(factory)generateCallHistoryDeclarations()→List<CodeDeclaration>(appended to impl file)- Returns
GeneratedFakeCode(implementationFile, factoryFunction)
8. Adding a New Generation Pattern
- Define the output — what Kotlin code should be generated
- Create spec (if needed) — add to
codegen/extensions/or extend existing specs - Add builder extensions — new functions on
ClassBuilderorCodeFileBuilder - Wire into orchestrator — call from
ImplementationGeneratororConfigurationDslGenerator - Add default strategy (if needed) — implement
DefaultValueStrategy, add toDefaultValueResolver - Test — verify with
make publish-local && make test-sample
Related Skills
feature-option— adding options that conditionally enable/disable generationcompilation— validate that generated output compiles