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

GitHub

Codegen 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:

  • Unit return 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:

  1. decl.toCodegenSpecs() → method and property specs
  2. generateCompleteFake() → CodeFile (impl class)
  3. generateFactoryFunctionCodeFile() → CodeFile (factory)
  4. generateCallHistoryDeclarations() → List<CodeDeclaration> (appended to impl file)
  5. Returns GeneratedFakeCode(implementationFile, factoryFunction)

8. Adding a New Generation Pattern

  1. Define the output — what Kotlin code should be generated
  2. Create spec (if needed) — add to codegen/extensions/ or extend existing specs
  3. Add builder extensions — new functions on ClassBuilder or CodeFileBuilder
  4. Wire into orchestrator — call from ImplementationGenerator or ConfigurationDslGenerator
  5. Add default strategy (if needed) — implement DefaultValueStrategy, add to DefaultValueResolver
  6. Test — verify with make publish-local && make test-sample

Related Skills

  • feature-option — adding options that conditionally enable/disable generation
  • compilation — validate that generated output compiles