How do you find all declarations annotated with a given annotation in a KSP processor, and how do you narrow the results to classes vs. functions?
answer
- getSymbolsWithAnnotation(fqName) -> Sequence<KSAnnotated>
- filterIsInstance<KSClassDeclaration / KSFunctionDeclaration>()
- validate() before using; defer the invalid
- KSAnnotated is the broad base type
- KSTypeReference.resolve() is expensive
basics
~10 sCall resolver.getSymbolsWithAnnotation("com.example.MyAnnotation") to get a sequence of annotated symbols, then filter to the kind you want, for example keeping only KSClassDeclaration or KSFunctionDeclaration.
solid answer
~30 sInside process(resolver), call resolver.getSymbolsWithAnnotation(annotationFqName: String): Sequence<KSAnnotated>. This returns every symbol carrying that annotation, regardless of kind. Because KSAnnotated is the broad base interface, you narrow by filtering on the concrete declaration type: filterIsInstance<KSClassDeclaration>() for classes/objects/interfaces, or filterIsInstance<KSFunctionDeclaration>() for functions and constructors. You almost always also filter on validity with .filter { it.validate() } so you skip symbols whose types aren't resolvable yet (typically because they reference not-yet-generated code), deferring them to a later round. A common idiom is to apply the KSVisitor pattern by calling symbol.accept(visitor, data) on each surviving declaration.
code
kotlin · 13 linesoverride fun process(resolver: Resolver): List<KSAnnotated> {
val annotated = resolver.getSymbolsWithAnnotation("com.example.Builder")
val deferred = annotated.filterNot { it.validate() }.toList()
annotated
.filter { it.validate() }
.filterIsInstance<KSClassDeclaration>()
.filter { it.classKind == ClassKind.CLASS && Modifier.DATA in it.modifiers }
.forEach { decl ->
val ctorParams = decl.primaryConstructor?.parameters.orEmpty()
// generate a builder from ctorParams ...
}
return deferred
}go deeper
Knows getSymbolsWithAnnotation exists and that you filter to the declaration you want.
Uses the fully-qualified name, filters with filterIsInstance to KSClassDeclaration/KSFunctionDeclaration, and reads members like parameters or primaryConstructor.
Adds validate()/deferral handling, lazy Sequence reasoning, and judicious resolve() use; understands classKind and modifiers.
Designs the query layer to minimize resolve() cost, handles multi-round correctness across processors, and reasons about determinism of generated output.
## The core query: getSymbolsWithAnnotation The **`Resolver`** is KSP's read-only query API over the whole compilation. The most common call: ```kotlin val symbols: Sequence<KSAnnotated> = resolver.getSymbolsWithAnnotation("com.example.Builder") ``` - The argument is the **fully-qualified name** of the annotation (string). - The return is a **`Sequence<KSAnnotated>`** — lazy, so filtering is cheap. - **`KSAnnotated`** is the base interface for anything that can carry annotations: classes, functions, properties, parameters. It does not by itself tell you the *kind* of declaration. An optional `inDepth: Boolean = false` parameter, when `true`, also searches local declarations inside function bodies (off by default for performance). ## Narrowing by declaration kind Filter to the concrete subtype you need: ```kotlin val classes = symbols.filterIsInstance<KSClassDeclaration>() // class/object/interface/enum val functions = symbols.filterIsInstance<KSFunctionDeclaration>() // functions + constructors ``` - **`KSClassDeclaration`** — a class-like declaration. Useful members: `classKind` (CLASS, INTERFACE, OBJECT, ENUM_CLASS, ANNOTATION_CLASS), `simpleName`, `qualifiedName`, `getAllProperties()`, `getDeclaredFunctions()`, `primaryConstructor`, `superTypes`, `typeParameters`. - **`KSFunctionDeclaration`** — a function or constructor. Useful members: `parameters` (each a `KSValueParameter`), `returnType` (a `KSTypeReference`), `modifiers` (e.g. `Modifier.SUSPEND`), `functionKind`. ## Validate and defer A symbol may reference types that **don't exist yet** because another processor (or your own earlier round) hasn't generated them. The `validate()` extension returns `false` for such symbols: ```kotlin val valid = symbols.filter { it is KSClassDeclaration && it.validate() } val deferred = symbols.filterNot { it.validate() }.toList() ``` Return the deferred ones from `process()` so KSP retries them next round. ## Resolving types To turn a `KSTypeReference` into a usable `KSType`, call `.resolve()`. Resolution is comparatively expensive, so do it only when needed. You can also fetch declarations directly: `resolver.getClassDeclarationByName(resolver.getKSNameFromString("kotlin.String"))`. ## Putting it together ```kotlin override fun process(resolver: Resolver): List<KSAnnotated> { val annotated = resolver.getSymbolsWithAnnotation("com.example.Builder") val (ready, deferred) = annotated.partition { it.validate() } ready.filterIsInstance<KSClassDeclaration>() .filter { it.classKind == ClassKind.CLASS } .forEach { generateBuilder(it) } return deferred } ``` ## Key terms - **KSAnnotated** — base interface for annotatable symbols. - **KSClassDeclaration / KSFunctionDeclaration** — concrete declaration kinds. - **validate()** — true when all referenced types are resolvable now. - **resolve()** — turns a `KSTypeReference` into a concrete `KSType`.
- Why filter with validate() before processing a symbol?A symbol may reference types that another round will generate; validate() is false until those types resolve. You defer such symbols (return them from process) so KSP retries them next round, avoiding crashes on unresolved references.
- What does getSymbolsWithAnnotation return, and why is it a Sequence?It returns Sequence<KSAnnotated> — lazy so chained filter/map operations don't materialize intermediate collections, which matters when scanning large codebases.
saying these in an interview costs you the question
- Assuming getSymbolsWithAnnotation only returns classes
- Calling resolve() on every type reference unconditionally
- Ignoring validate() and crashing on unresolved symbols
- Passing a simple name instead of the fully-qualified annotation name
- Materializing the whole sequence with toList() before filtering