Kotlin-specific features and guidance
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:

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 auxClasspath configured. In that setup, cross-package import-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 configuring auxClasspath to include the type as a compiled class (a jar, or another module’s target/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: @TypeName and @ReturnTypeName use fully-qualified names for generic type arguments: kotlin.collections.List<kotlin.String>?, not List<String>?. When comparing in XPath, use the full FQN form: @TypeName = 'kotlin.collections.List<kotlin.String>'. For type checks that ignore generics, prefer pmd-kotlin:typeIs() or pmd-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: VariableDeclaration carries only @Identifier (the variable name) and @TypeName. Modifiers like private, lateinit, or const are on the parent PropertyDeclaration node. Use //PropertyDeclaration[pmd-kotlin:modifiers() = 'private'] rather than querying VariableDeclaration.

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 destructuring PropertyDeclaration has no single type, so //PropertyDeclaration[pmd-kotlin:typeIs(...)] misses destructured variables. The implicit lambda parameter it has no declaration node and no @TypeName yet.

Note: Boolean attributes like @Mutable require XPath’s true() / false() functions: //PropertyDeclaration[@Mutable=false()] (immutable val declarations). Using false without 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/@Value attribute.
  • 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 rich JTypeMirror objects (never null, using UNKNOWN for unresolved types), pmd-kotlin exposes KotlinTypeName with string-based type names — reflecting the simpler output of kotlin-type-mapper. A richer type model may be added in a future version.

Language Properties

See Kotlin language properties