agentleFS
rsicarellirsicarelli/fakt/.claude/skills/codegen/SKILL.md

codegen

Skill67 starsChanged 44 days agoApache-2.0View on GitHub

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 output, adding new generated patterns, understanding builders or renderers, adding a new default value strategy, or debugging generated output. Make sure to use this skill whenever generated Kotlin code needs to change — the pipeline has strict conventions (immutable AST, builder DSL, renderer separation) that must be followed even for small changes.

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

1 rule. 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All codegen files live under codegen-runtime/src/main/kotlin/com/rsicarelli/fakt/codegen/.

    line 31

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

TypePurpose
CodeFileRoot: package + imports + declarations
CodeDeclarationSealed: CodeClass, CodeDataClass, CodeFunction, CodeProperty
CodeMemberSealed: class body items (CodeFunction, CodeProperty, CodeComment, CodeRegionStart/End)
CodeClassClass with modifiers, supertypes, members, constructor properties
CodeDataClassData class with typed properties
CodeFunctionFunction with params, return type, body
CodePropertyProperty with initializer, getter/setter
CodeTypeSealed: Simple, Generic, Nullable, Lambda
CodeBlockSealed: Expression, Statements, Empty
CodeExpressionSealed: StringLiteral, NumberLiteral, Lambda, FunctionCall, Raw
CodeModifierEnum: PUBLIC, PRIVATE, INTERNAL, OVERRIDE, SUSPEND, INLINE, etc.
ConstructorPropertyConstructor 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:

PriorityStrategyHandles
1NullableDefaultStrategyT? → null
2PrimitiveDefaultStrategyString → "", Int → 0, Boolean → false, etc.
3StdlibDefaultStrategyUnit, Flow<T> → emptyFlow(), StateFlow<T> → MutableStateFlow(...), Result<T>
4CollectionDefaultStrategyList<T> → emptyList(), Map<K,V> → emptyMap(), Array<T>, Mutable*
fallbackerror"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:

SpecPurpose
MethodSpecMethod metadata: name, params, return type, suspend, operator, default behavior
PropertySpecProperty metadata: name, type, isStateFlow, isMutable, default behavior
AnnotationSpecAnnotation with OptIn detection
FactoryFunctionSpecFactory function metadata for fakeXxx {} generation

Extension functions on builders (in separate files):

FileFunctionsPurpose
FakeGenerator.ktgenerateCompleteFake()Main fake class generation
MethodExtensions.ktoverrideMethod(), overrideVarargMethod(), configureMethod()Method overrides + configure methods
PropertyExtensions.ktstateFlowProperty(), behaviorProperty(), suspendBehaviorProperty(), nullableBehaviorProperty(), nullableSuspendBehaviorProperty()Property overrides + behavior lambdas
CallTrackingExtensions.ktcallHistoryBackingField(), propertyGetterCallHistoryField(), propertySetterCallHistoryField()Call tracking fields
CallHistoryDsl.ktcallDataClass(), verifierClass(), unitVerifierClass(), verifyFunction(), unitVerifyFunction()Call history data classes + verifiers
CallHistoryGenerator.ktaddCallHistoryComponents(), generateCallHistoryDeclarations()Orchestrates all call history code
FactoryExtensions.ktgenerateFactoryFunctionCodeFile()fakeXxx {} factory function
TypeParamRegex.kteraseTypeParamsToAny(), typeContainsAnyParam()Generic type erasure utilities
HistoryStorageInfo.ktresolveHistoryStorageType()Resolves call history storage (unit vs data class)
KDocGenerator.ktKDoc generation utilitiesGenerated documentation for fakes
TypeParamsInfo.ktType parameter analysis helpersType parameter metadata extraction
VisibilityExtensions.ktVisibility mapping helpersFIR visibility to codegen modifier mapping

7. Generation Orchestrators — codegen/generator/ (mapper in codegen/analysis/)

FileClassRole
analysis/SpecToCodegenMapper.kt—FakeDeclaration.toCodegenSpecs(), toRenderableMethodSpec(), toRenderablePropertySpec()
generator/ImplementationGenerator.ktImplementationGeneratorProduces FakeXxxImpl class + factory function + call history
generator/ConfigurationDslGenerator.ktConfigurationDslGeneratorProduces 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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.