Skip to main content

Kotlin Linting and Static Analysis

Kotlin's type system and null safety catch many errors at compile time, but linting and static analysis tools enforce code style, detect code smells, and catch additional issues beyond compilation errors. A comprehensive linting setup ensures code quality, maintainability, and consistency across Kotlin projects.

Overview

While the Kotlin compiler ensures type safety and null safety, linting tools ensure code quality and adherence to best practices. The Kotlin linting ecosystem includes:

Core tools:

  • ktlint - Code formatting and style enforcement (Kotlin's de facto standard formatter)
  • detekt - Static code analysis for code smells, complexity, and best practices
  • Android Lint - Android-specific checks (for Android projects)
  • Kotlin compiler warnings - Built-in compiler diagnostics

These tools integrate seamlessly with Gradle, IntelliJ IDEA, and CI/CD pipelines. Unlike Java which has many competing formatters, Kotlin has converged on ktlint as the community standard, similar to how Go uses gofmt.

Why multiple tools: ktlint handles formatting (whitespace, naming, structure), while detekt analyzes code quality (complexity, potential bugs, performance issues). Use both for comprehensive coverage. See our Pipeline Configuration for CI/CD integration patterns.


Code Formatting with ktlint

ktlint is the standard code formatter and style checker for Kotlin. It enforces the official Kotlin coding conventions and integrates with build tools to automatically format code or verify formatting in CI pipelines.

Why ktlint

Manual code formatting leads to:

  • Inconsistent style across the codebase
  • Wasted reviewer time on style feedback
  • Merge conflicts from formatting differences
  • Cognitive load from varying styles

ktlint eliminates these problems by providing one consistent, automated format. Unlike Java's ecosystem with multiple competing formatters (Google Java Format, Eclipse formatter), Kotlin has largely standardized on ktlint, making it the obvious choice.

Key features:

  • Enforces Kotlin's official coding conventions
  • Zero configuration required (opinionated defaults)
  • Auto-fix capability for most violations
  • Gradle, Maven, and Git hooks integration
  • Fast - typically completes in seconds even on large projects

Configuration

// build.gradle.kts
plugins {
id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
}

ktlint {
version.set("1.0.1")
android.set(true) // Enable Android-specific rules if applicable

// Optional: disable specific rules
disabledRules.set(setOf(
"no-wildcard-imports", // Allow wildcard imports if desired
"max-line-length" // Disable if you have custom length requirements
))

// Reporters
reporters {
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
}

Groovy syntax (build.gradle):

plugins {
id "org.jlleitschuh.gradle.ktlint" version "12.0.3"
}

ktlint {
version = "1.0.1"
android = true

disabledRules = ["no-wildcard-imports", "max-line-length"]
}

Running ktlint

# Check formatting (fails on violations)
./gradlew ktlintCheck

# Auto-fix formatting issues
./gradlew ktlintFormat

# Check specific source set
./gradlew ktlintMainSourceSetCheck
./gradlew ktlintTestSourceSetCheck

# Android-specific
./gradlew ktlintDebugCheck

Common ktlint Rules

RuleDescriptionAuto-fix
indentEnforce 4-space indentationYes
no-trailing-spacesRemove trailing whitespaceYes
final-newlineFiles must end with newlineYes
no-consecutive-blank-linesMax one blank line between codeYes
no-wildcard-importsAvoid import com.example.*Yes
max-line-lengthLines should not exceed 120 charactersNo (manual refactoring)
parameter-list-wrappingWrap long parameter listsYes
string-templatePrefer string templates over concatenationYes
annotation-spacingCorrect spacing around annotationsYes

IDE Integration

IntelliJ IDEA and Android Studio can use ktlint's rules directly:

IntelliJ IDEA:

# Apply ktlint rules to IDE
./gradlew ktlintApplyToIdea

# Or install ktlint plugin from marketplace
# Settings → Plugins → Search "ktlint"

Editor Config: ktlint generates .editorconfig automatically. Commit this file to ensure consistent formatting across editors:

./gradlew ktlintApplyToIdea  # Generates .editorconfig
git add .editorconfig

The generated .editorconfig ensures that VS Code, IntelliJ, and other editors use consistent formatting settings.

Pre-commit Hook

Integrate ktlint into Git hooks to catch formatting issues before commits:

# .git/hooks/pre-commit
#!/bin/bash

echo "Running ktlint check..."
./gradlew ktlintCheck

if [ $? -ne 0 ]; then
BAD: echo " ktlint found formatting issues."
echo "Run './gradlew ktlintFormat' to fix automatically."
exit 1
fi

GOOD: echo " ktlint check passed!"

Alternatively, use the Gradle plugin's built-in hook installation:

ktlint {
installGitPreCommitHook.set(true) // Automatically installs pre-commit hook
}

Static Analysis with detekt

detekt performs static code analysis to detect code smells, complexity issues, potential bugs, and violations of best practices. While ktlint handles formatting, detekt handles code quality.

Why detekt

detekt catches issues that compile successfully but indicate poor code quality:

  • High complexity: Functions with too many branches or nested logic
  • Code smells: Long methods, god classes, feature envy
  • Potential bugs: Empty catch blocks, platform calls on non-platform types, incorrect exception handling
  • Performance: Inefficient collection operations, unnecessary object creation
  • Security: Hardcoded credentials, insecure random number generation

These issues slow down development, increase bugs, and make code harder to maintain. detekt catches them automatically, freeing code reviewers to focus on architecture and business logic.

Configuration

// build.gradle.kts
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.4"
}

detekt {
buildUponDefaultConfig = true
allRules = false // Don't enable ALL rules by default
config.setFrom(files("$projectDir/config/detekt/detekt.yml"))

reports {
html.required.set(true)
xml.required.set(true)
txt.required.set(true)
sarif.required.set(true) // For GitHub integration
}
}

dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4")
}

Custom configuration (config/detekt/detekt.yml):

# config/detekt/detekt.yml
build:
maxIssues: 0 # Fail build on any issue
excludeCorrectable: false

complexity:
LongMethod:
threshold: 60 # Max lines per method
LongParameterList:
functionThreshold: 6
constructorThreshold: 7
CyclomaticComplexMethod:
threshold: 15 # Max cyclomatic complexity

code-smell:
LongMethod:
active: true
LongParameterList:
active: true
TooManyFunctions:
active: true
thresholdInFiles: 15
thresholdInClasses: 15

empty-blocks:
EmptyCatchBlock:
active: true
EmptyFinallyBlock:
active: true

exceptions:
TooGenericExceptionCaught:
active: true
exceptionNames:
- Error
- Exception
- Throwable
- RuntimeException

naming:
FunctionNaming:
active: true
functionPattern: '[a-z][a-zA-Z0-9]*'
ClassNaming:
active: true
classPattern: '[A-Z][a-zA-Z0-9]*'

performance:
SpreadOperator:
active: true # Warn about performance issues with spread operator

potential-bugs:
Deprecation:
active: true
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
InvalidRange:
active: true

style:
MagicNumber:
active: true
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
MaxLineLength:
active: true
maxLineLength: 120

Running detekt

# Run analysis
./gradlew detekt

# Generate baseline (ignore existing issues)
./gradlew detektBaseline

# With type resolution (slower but more accurate)
./gradlew detektMain detektTest

Common detekt Rules

Complexity:

  • ComplexMethod - Methods with high cyclomatic complexity
  • LongMethod - Methods exceeding line threshold
  • LongParameterList - Too many parameters
  • NestedBlockDepth - Deeply nested if/for/while statements

Code Smells:

  • LongClass - Classes with too many lines
  • TooManyFunctions - Classes with too many methods
  • LargeClass - Classes with too many fields

Potential Bugs:

  • EqualsWithHashCodeExist - equals() without hashCode()
  • DuplicateCaseInWhenExpression - Duplicate when branches
  • InvalidRange - Invalid range expressions
  • UnsafeCallOnNullableType - Potential null pointer access

Performance:

  • SpreadOperator - Performance issues with spread operator in loops
  • ForEachOnRange - Use indices instead of forEach on ranges

Style:

  • MagicNumber - Hardcoded numbers without named constants
  • ForbiddenComment - Comments like TODO, FIXME without tickets
  • UnusedImports - Unused import statements

Baseline for Legacy Code

For large existing codebases with many violations, create a baseline to ignore existing issues while preventing new ones:

# Create baseline file
./gradlew detektBaseline

# detekt will generate detekt-baseline.xml
# Commit this file
git add detekt-baseline.xml

With a baseline, detekt only reports new violations, allowing gradual improvement without blocking development. Remove the baseline once all issues are fixed.


Android Lint (Android Projects Only)

Android Lint provides Android-specific static analysis, catching issues like:

  • Missing translations in string resources
  • Incorrect API usage (deprecated APIs, wrong API levels)
  • Performance issues (overdraw, memory leaks)
  • Security vulnerabilities (exported components without permissions)
  • Accessibility issues (missing content descriptions)

Configuration

// build.gradle.kts (Android module)
android {
lint {
abortOnError = true // Fail build on errors
warningsAsErrors = true // Treat warnings as errors
checkAllWarnings = true

// Disable specific checks if needed
disable += setOf(
"Typos", // Too many false positives
"ObsoleteLintCustomCheck"
)

// Output formats
htmlReport = true
xmlReport = true

baseline = file("lint-baseline.xml") // Baseline for legacy code
}
}

Running Android Lint

# Run lint checks
./gradlew lint

# Generate lint report
./gradlew lintDebug lintRelease

# Create baseline
./gradlew lintDebug --continue
# Copy generated lint-results-debug.xml to lint-baseline.xml

Common Android Lint Checks

CheckDescriptionSeverity
HardcodedTextText not extracted to strings.xmlWarning
UseCompoundDrawablesCan use compound drawable for better performanceWarning
OverdrawToo many overlapping views (performance)Warning
UnusedResourcesUnused layout, drawable, or string resourcesWarning
MissingPermissionMissing required permission in manifestError
NewApiUsing APIs newer than minSdkVersionError
RtlHardcodedHardcoded left/right (should use start/end)Warning
ContentDescriptionMissing accessibility description on ImageViewWarning

For comprehensive Android-specific guidelines, see our Android Code Review guide.


Compiler Warnings

The Kotlin compiler provides extensive warnings about potential issues. Enable all warnings and treat them as errors:

// build.gradle.kts
kotlin {
compilerOptions {
allWarningsAsErrors.set(true) // Treat all warnings as errors
freeCompilerArgs.add("-Xjsr305=strict") // Strict null-safety for Java interop
}
}

Common compiler warnings:

  • Deprecation: Using deprecated APIs
  • Unused: Unused variables, parameters, imports
  • Unchecked cast: Type cast that cannot be verified at runtime
  • Unnecessary safe call: ?. on non-nullable type
  • Platform type: Java type without null-safety information
// BAD: Compiler warning: Unnecessary safe call
val name: String = "test"
val length = name?.length // Warning: Unnecessary safe call on non-null receiver

// GOOD: Fixed
val length = name.length

// BAD: Compiler warning: Unused parameter
fun process(payment: Payment, unused: String) { // Warning: Parameter 'unused' is never used
println(payment.amount)
}

// GOOD: Fixed: Remove unused parameter or prefix with _
fun process(payment: Payment, _metadata: String) {
println(payment.amount)
}

Suppressing Warnings

When suppression is necessary, use @Suppress with minimal scope and a comment explaining why:

// GOOD: Suppress with narrow scope and justification
@Suppress("UNCHECKED_CAST") // Legacy API returns raw type
fun <T> fromJson(json: String): T {
return gson.fromJson(json, Any::class.java) as T
}

// BAD: Suppressing entire file
@file:Suppress("UNCHECKED_CAST", "DEPRECATION") // Too broad

// BAD: No justification
@Suppress("DEPRECATION")
fun oldApi() { ... }

Pre-commit Hooks for Fast Feedback

Integrate linting into Git pre-commit hooks to catch issues before they're committed:

#!/bin/bash
# .git/hooks/pre-commit

echo "Running ktlint check..."
./gradlew ktlintCheck
if [ $? -ne 0 ]; then
BAD: echo " ktlint failed. Run './gradlew ktlintFormat' to fix."
exit 1
fi

echo "Running detekt..."
./gradlew detekt
if [ $? -ne 0 ]; then
BAD: echo " detekt found issues. Review and fix violations."
exit 1
fi

echo "Compiling..."
./gradlew compileKotlin compileTestKotlin
if [ $? -ne 0 ]; then
BAD: echo " Compilation failed."
exit 1
fi

GOOD: echo " Pre-commit checks passed!"

Performance tip: Pre-commit hooks should complete in under 30 seconds. If detekt is too slow, consider running only ktlint in pre-commit and running detekt only in CI.


CI/CD Integration

Static analysis must run in CI/CD pipelines to enforce quality gates. Configure pipelines to fail builds when quality thresholds aren't met.

GitHub Actions Example

# .github/workflows/quality.yml
name: Code Quality

on: [pull_request]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

- name: Run ktlint
run: ./gradlew ktlintCheck

- name: Run detekt
run: ./gradlew detekt

- name: Upload detekt report
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: build/reports/detekt/detekt.sarif

GitLab CI Example

# .gitlab-ci.yml
quality:
stage: test
image: openjdk:17-jdk
script:
- ./gradlew ktlintCheck
- ./gradlew detekt
- ./gradlew compileKotlin compileTestKotlin
cache:
paths:
- .gradle/
artifacts:
reports:
junit: build/test-results/test/TEST-*.xml
paths:
- build/reports/
allow_failure: false

See our CI/CD Pipeline Configuration for more examples.


Tool Comparison

ToolPurposeSpeedAuto-fixIntegration
ktlintCode formattingVery FastYesGradle, pre-commit, CI
detektStatic analysisMediumSome rulesGradle, CI
Android LintAndroid-specific checksMedium-SlowNoGradle, Android Studio, CI
Compiler warningsType safety, nullabilityFastNoAlways on

Minimal (small team/starting out):

  • ktlint for formatting
  • Compiler warnings as errors
  • Pre-commit hook for ktlint

Comprehensive (production apps):

  • ktlint for formatting
  • detekt for static analysis
  • Android Lint (if Android)
  • Compiler warnings as errors
  • Pre-commit hooks
  • CI enforcement with quality gates

Android-specific (in addition to above):

  • Android Lint with custom rules
  • Baseline for legacy code
  • SARIF output for GitHub integration

Best Practices

1. Format Early, Format Often

Run ktlintFormat before committing:

./gradlew ktlintFormat && git add -A

2. Use Baselines for Legacy Code

Don't let existing issues block new development:

./gradlew detektBaseline
git add detekt-baseline.xml

3. Customize Carefully

ktlint and detekt have sensible defaults. Only disable rules if they genuinely don't fit your project - document why in comments.

4. Integrate with IDE

Apply ktlint rules to IDE for immediate feedback:

./gradlew ktlintApplyToIdea

5. Fail Fast in CI

Configure CI to fail immediately on linting errors - don't waste time running tests if code doesn't meet quality standards.

6. Review Reports

Generated HTML reports (detekt, Android Lint) provide detailed explanations. Review these to understand violations, not just the console output.

7. Gradual Improvement

For large existing codebases:

  1. Create baselines for all tools
  2. Fix new violations immediately
  3. Allocate time each sprint to fix baseline violations
  4. Track progress - remove items from baseline as they're fixed

Further Reading

Internal Documentation

External Resources


Summary

Key Takeaways

  1. ktlint - Use for formatting (de facto Kotlin standard)
  2. detekt - Use for static analysis and code quality
  3. Android Lint - Essential for Android projects
  4. Compiler warnings - Treat all warnings as errors
  5. Pre-commit hooks - Catch issues before they enter the repository
  6. CI enforcement - Quality gates prevent low-quality code from merging
  7. Baselines - Handle legacy code without blocking new development
  8. IDE integration - Format-on-save provides immediate feedback
  9. Gradual improvement - Use baselines and fix incrementally
  10. Sensible defaults - Don't over-customize rules without good reason

Next Steps: Configure these tools in your project, integrate them into CI/CD pipelines, and review the Kotlin Testing guide for testing strategies.