Table of Contents
Kotlin support in PMD is based on the official grammar from https://github.com/Kotlin/kotlin-spec.
Language Info for Kotlin
- Since PMD 7.0
- Implementation:
KotlinLanguageModule - Id: kotlin
- PMD: ✔️
- CPD: ✔️
Java-based rules and XPath-based rules are supported.
Kotlin language versions / feature support
PMD aims to support Kotlin up to version 2.2.0, which is also the highest version PMD advertises. The parser is based on the official Kotlin grammar, but it may lag behind the Kotlin compiler, so some newer language features may not be parsed correctly yet. If you find Kotlin code that PMD fails to parse, please report an issue. PRs to improve Kotlin parser coverage are welcome.
Selecting a different Kotlin language version does not currently change the parser behavior (unlike Java).
Examples of newer language features that are supported:
- Kotlin 1.9:
..<operator for open-ended ranges (#7171) - Kotlin 2.2: Multi-dollar string interpolation (#6003) (preview in 2.1.0, stable in 2.2.0)
Providing the auxiliary classpath
The auxiliary classpath (or short “auxClasspath”) is configured via the
Language Property “auxClasspath”.
It is a string containing multiple paths separated by either a colon (:) under Linux/MacOS
or a semicolon (;) under Windows. This property can be provided on the CLI with parameter
--aux-classpath.
Not providing the correct auxClasspath might result in false positives or negatives for some rules, that depend on resolved types.
Note for rule testing (rule test XML, PMD Designer): these typically analyze one isolated Kotlin snippet/file at a time with no
auxClasspathconfigured. In that setup, cross-packageimport-based type resolution to another Kotlin source file does not work yet – only same-package cross-file resolution and same-file resolution do. If a test snippet imports a type from a different package, resolve it either by putting both types in the same package, or by configuringauxClasspathto include the type as a compiled class (a jar, or another module’starget/classes), which resolves correctly regardless of package.
Type resolution for Kotlin is powered by kotlin-type-mapper, which is bundled with PMD.
XPath rule support
Kotlin provides XPath-queryable attributes and helper functions for type-aware rules.
Type-info attributes
The following attributes depend on type resolution: they are only available when auxClasspath
is configured and the kotlin-type-mapper analysis has resolved the types.
| Attribute | Nodes | Meaning |
|---|---|---|
@TypeName |
PropertyDeclaration (absent for destructuring declarations), VariableDeclaration (property and for-loop variables, lambda parameters, and each component of a destructuring declaration such as val (a, b) = ...), ClassParameter, FunctionValueParameter, CatchBlock, ForStatement, ClassDeclaration, DelegationSpecifier, UnescapedAnnotation, SingleAnnotation |
Fully-qualified type name (including generic type arguments and nullable marker, e.g. kotlin.collections.List<kotlin.String>?) |
@ReturnTypeName |
FunctionDeclaration |
Fully-qualified return type name (including generic type arguments and nullable marker) |
@AnnotationFqNames |
FunctionDeclaration, ClassDeclaration, PropertyDeclaration, ClassParameter |
Sequence of FQNs of all annotations on the declaration |
A type-info attribute is absent (not present with a null value) whenever its value is unavailable.
Note:
@TypeNameand@ReturnTypeNameuse fully-qualified names for generic type arguments:kotlin.collections.List<kotlin.String>?, notList<String>?. When comparing in XPath, use the full FQN form:@TypeName = 'kotlin.collections.List<kotlin.String>'. For type checks that ignore generics, preferpmd-kotlin:typeIs()orpmd-kotlin:typeIsExactly().
General attributes
These attributes are structural: they come directly from the parsed source and don’t depend on
type resolution, so they’re always present regardless of auxClasspath.
| Attribute | Nodes | Meaning |
|---|---|---|
@Mutable |
PropertyDeclaration |
true for var, false for val. |
@Identifier |
ClassDeclaration, FunctionDeclaration, ClassParameter, CompanionObject, VariableDeclaration, ImportAlias |
Simple name of the declared identifier |
@Name |
ImportHeader |
Fully-qualified imported name (e.g. kotlin.collections.listOf). |
Note:
VariableDeclarationcarries only@Identifier(the variable name) and@TypeName. Modifiers likeprivate,lateinit, orconstare on the parentPropertyDeclarationnode. Use//PropertyDeclaration[pmd-kotlin:modifiers() = 'private']rather than queryingVariableDeclaration.
Tip: To find variables by type, query
VariableDeclaration, e.g.//VariableDeclaration[pmd-kotlin:typeIs('java.util.Calendar')]. It covers property and for-loop variables, lambda parameters ({ x -> ... },{ x: String -> ... }) as well as each component of a destructuring declaration (val (a, b) = ...,for ((a, b) in ...),{ (a, b) -> ... }). A destructuringPropertyDeclarationhas no single type, so//PropertyDeclaration[pmd-kotlin:typeIs(...)]misses destructured variables. The implicit lambda parameterithas no declaration node and no@TypeNameyet.
Note: Boolean attributes like
@Mutablerequire XPath’strue()/false()functions://PropertyDeclaration[@Mutable=false()](immutablevaldeclarations). Usingfalsewithout parentheses matches a node name, not a boolean value.
Absent value: unresolved vs genuinely none
An absent attribute is ambiguous on its own — the type could be unresolved, or genuinely not
present. Note that a configured auxClasspath does not guarantee every type resolves (incomplete
classpath, generated/preprocessed code, missing annotation processors, etc.).
pmd-kotlin:hasUnresolvedReference() — whether the node has an unresolved reference (the Kotlin
analog of pmd-java’s isUnresolved()) — disambiguates the two cases:
hasUnresolvedReference() |
Attribute absent means |
|---|---|
| true | unresolved — analysis ran but the type could not be resolved |
| false | genuinely none — analysis ran and resolved; there is no such value |
(: property definitely without a resolvable declared type (not merely unresolved) :)
//PropertyDeclaration[not(@TypeName) and not(pmd-kotlin:hasUnresolvedReference())]
Limitation:
hasUnresolvedReference()only checks the context node’s begin line and only returns a boolean — it does not tell you which symbol failed to resolve, and it won’t see an unresolved reference on a different line of a multi-line declaration. Exposing the unresolved reference name(s) is tracked as a possible follow-up function.
XPath functions
There are a number of XPath functions available in the pmd-kotlin namespace, e.g.
typeIs(), isNullable(), hasAnnotation(), and more. For the full list with descriptions,
parameters, and examples, see the
PMD extension functions
page.
Message parameter ({0}) substitution
net.sourceforge.pmd.lang.rule.xpath.XPathRule auto-binds {0} in a rule’s message from the
matched node: it tries getImage() first, then falls back to the first attribute named Name,
SimpleName, MethodName, or Value — this fallback list is hardcoded in pmd-core and shared
across all languages.
pmd-kotlin nodes have getImage() intentionally disabled (@NoAttribute @Deprecated), and most
Kotlin declaration nodes don’t expose any of the hardcoded fallback attribute names either (a
notable exception is ImportHeader’s @Name). So for a typical Kotlin XPathRule, {0} won’t
resolve to anything useful unless the matched node happens to carry one of those specific
attributes.
If your rule’s message needs a dynamic value (e.g. the offending type name), consider one of:
- Matching a node that does carry a usable
@Name/@SimpleName/@MethodName/@Valueattribute. - Writing a Java-based rule instead of an XPathRule, and calling
ctx.addViolation(node, arg)explicitly with any value you like. - Keeping the message static/generic — this is the current approach for
LooseCoupling, since the flagged concrete type name isn’t exposed under any of the fallback attribute names.
Java-based rules
The same type information is accessible from Java-based rules via public interfaces and
KotlinNodeTypeData:
import net.sourceforge.pmd.lang.kotlin.ast.HasTypeName;
import net.sourceforge.pmd.lang.kotlin.ast.HasModifiers;
import net.sourceforge.pmd.lang.kotlin.ast.KotlinParser.KtKotlinFile;
import net.sourceforge.pmd.lang.kotlin.types.KotlinNodeTypeData;
import net.sourceforge.pmd.lang.kotlin.types.KotlinTypeName;
// Check type on a node that implements HasTypeName
// (PropertyDeclaration, FunctionDeclaration, ClassParameter, etc.)
if (node instanceof HasTypeName) {
KotlinTypeName type = ((HasTypeName) node).getType(); // null if unresolved
if (type != null) {
String fqName = type.getFqName(); // e.g. "kotlin.collections.List"
boolean nullable = type.isNullable(); // true if declared as nullable (?)
String display = type.toDisplayString(); // e.g. "kotlin.collections.List<kotlin.String>?"
}
}
// Get modifiers on declaration nodes implementing HasModifiers
// (Java-only API: not exposed as @Modifiers XPath attribute; use
// pmd-kotlin:modifiers() from XPath instead)
if (node instanceof HasModifiers) {
List<String> mods = ((HasModifiers) node).getModifiers(); // e.g. ["override", "suspend"]
}
// Static helpers on KotlinNodeTypeData (work on any KotlinNode)
KotlinTypeName type = KotlinNodeTypeData.getType(node);
KotlinTypeName returnType = KotlinNodeTypeData.getReturnType(node);
List<String> annotations = KotlinNodeTypeData.getAnnotationFqNames(node);
All type getters return null when type analysis has not run or the type could not be resolved.
The KotlinTypeName class provides structured access to the FQ name, nullability, resolution
status, and full display string (including generic type arguments).
Note: Unlike pmd-java’s
TypeNode.getTypeMirror()which returns richJTypeMirrorobjects (never null, usingUNKNOWNfor unresolved types), pmd-kotlin exposesKotlinTypeNamewith string-based type names — reflecting the simpler output of kotlin-type-mapper. A richer type model may be added in a future version.