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
| Rule | Description | Auto-fix |
|---|---|---|
indent | Enforce 4-space indentation | Yes |
no-trailing-spaces | Remove trailing whitespace | Yes |
final-newline | Files must end with newline | Yes |
no-consecutive-blank-lines | Max one blank line between code | Yes |
no-wildcard-imports | Avoid import com.example.* | Yes |
max-line-length | Lines should not exceed 120 characters | No (manual refactoring) |
parameter-list-wrapping | Wrap long parameter lists | Yes |
string-template | Prefer string templates over concatenation | Yes |
annotation-spacing | Correct spacing around annotations | Yes |
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 complexityLongMethod- Methods exceeding line thresholdLongParameterList- Too many parametersNestedBlockDepth- Deeply nested if/for/while statements
Code Smells:
LongClass- Classes with too many linesTooManyFunctions- Classes with too many methodsLargeClass- Classes with too many fields
Potential Bugs:
EqualsWithHashCodeExist-equals()withouthashCode()DuplicateCaseInWhenExpression- DuplicatewhenbranchesInvalidRange- Invalid range expressionsUnsafeCallOnNullableType- Potential null pointer access
Performance:
SpreadOperator- Performance issues with spread operator in loopsForEachOnRange- Use indices instead offorEachon ranges
Style:
MagicNumber- Hardcoded numbers without named constantsForbiddenComment- Comments like TODO, FIXME without ticketsUnusedImports- 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
| Check | Description | Severity |
|---|---|---|
HardcodedText | Text not extracted to strings.xml | Warning |
UseCompoundDrawables | Can use compound drawable for better performance | Warning |
Overdraw | Too many overlapping views (performance) | Warning |
UnusedResources | Unused layout, drawable, or string resources | Warning |
MissingPermission | Missing required permission in manifest | Error |
NewApi | Using APIs newer than minSdkVersion | Error |
RtlHardcoded | Hardcoded left/right (should use start/end) | Warning |
ContentDescription | Missing accessibility description on ImageView | Warning |
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
| Tool | Purpose | Speed | Auto-fix | Integration |
|---|---|---|---|---|
| ktlint | Code formatting | Very Fast | Yes | Gradle, pre-commit, CI |
| detekt | Static analysis | Medium | Some rules | Gradle, CI |
| Android Lint | Android-specific checks | Medium-Slow | No | Gradle, Android Studio, CI |
| Compiler warnings | Type safety, nullability | Fast | No | Always on |
Recommended Setup
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:
- Create baselines for all tools
- Fix new violations immediately
- Allocate time each sprint to fix baseline violations
- Track progress - remove items from baseline as they're fixed
Further Reading
Internal Documentation
- Kotlin General Guidelines - Core Kotlin best practices
- Kotlin Testing - Testing strategies
- Android Code Review - Android-specific linting
- Code Quality Metrics - Measuring code quality
- CI/CD Pipeline Configuration - Pipeline integration
External Resources
Summary
Key Takeaways
- ktlint - Use for formatting (de facto Kotlin standard)
- detekt - Use for static analysis and code quality
- Android Lint - Essential for Android projects
- Compiler warnings - Treat all warnings as errors
- Pre-commit hooks - Catch issues before they enter the repository
- CI enforcement - Quality gates prevent low-quality code from merging
- Baselines - Handle legacy code without blocking new development
- IDE integration - Format-on-save provides immediate feedback
- Gradual improvement - Use baselines and fix incrementally
- 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.