Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Detekt - State of the Union

Detekt - State of the Union

Do you know detekt? We are The static analyzer for Kotlin, trusted by millions of developers around the globe, and completely built by the open-source community.

Our mission: spotting bugs, anti-patterns, and potential errors in your Kotlin code.

Did you know that you can extend detekt with your custom rules?
Did you know that we recently launched support for Kotlin Multiplatform?
Did you know that we're always looking for new contributors?

Whether you're new to detekt or you're a veteran, this talk is for you!
We will walk through the tool, and give you updates on the latest features we shipped and what comes next for the future.

Nicola Corti

June 09, 2021
Tweet

More Decks by Nicola Corti

Other Decks in Technology

Transcript

  1. Rulesets • comments • complexity ComplexCondition ComplexInterface ComplexMethod LabeledExpression LargeClass

    LongMethod LongParameterList MethodOverloading NamedArguments NestedBlockDepth ReplaceSafeCallChainWithRun StringLiteralDuplication TooManyFunctions
  2. Rulesets • comments • complexity • coroutines • empty-blocks EmptyCatchBlock

    EmptyClassBlock EmptyDefaultConstructor EmptyDoWhileBlock EmptyElseBlock EmptyFinallyBlock EmptyForBlock EmptyFunctionBlock EmptyIfBlock EmptyInitBlock EmptyKtFile EmptySecondaryConstructor EmptyTryBlock EmptyWhenBlock EmptyWhileBlock
  3. Rulesets • comments • complexity • coroutines • empty-blocks •

    exceptions ExceptionRaisedInUnexpectedLocation InstanceOfCheckForException NotImplementedDeclaration ObjectExtendsThrowable PrintStackTrace RethrowCaughtException ReturnFromFinally SwallowedException ThrowingExceptionFromFinally ThrowingExceptionInMain ThrowingExceptionsWithoutMessage ThrowingNewInstanceOfSameException TooGenericExceptionCaught TooGenericExceptionThrown
  4. Rulesets • comments • complexity • coroutines • empty-blocks •

    exceptions • naming ClassNaming ConstructorParameterNaming EnumNaming ForbiddenClassName FunctionMaxLength FunctionMinLength FunctionNaming FunctionParameterNaming InvalidPackageDeclaration MatchingDeclarationName MemberNameEqualsClassName NoNameShadowing NonBooleanPropertyPrefixedWithIs ObjectPropertyNaming PackageNaming TopLevelPropertyNaming VariableMaxLength VariableMinLength VariableNaming
  5. Rulesets • comments • complexity • coroutines • empty-blocks •

    exceptions • naming • performance ArrayPrimitive ForEachOnRange SpreadOperator UnnecessaryTemporaryInstantiation
  6. Rulesets • comments • complexity • coroutines • empty-blocks •

    exceptions • naming • performance • potential-bugs CastToNullableType Deprecation DontDowncastCollectionTypes DoubleMutabilityForCollection DuplicateCaseInWhenExpression EqualsAlwaysReturnsTrueOrFalse EqualsWithHashCodeExist ExitOutsideMain ExplicitGarbageCollectionCall HasPlatformType IgnoredReturnValue ImplicitDefaultLocale ImplicitUnitReturnType InvalidRange IteratorHasNextCallsNextMethod IteratorNotThrowingNoSuchElementException LateinitUsage MapGetWithNotNullAssertionOperator MissingWhenCase NullableToStringCall RedundantElseInWhen UnconditionalJumpStatementInLoop UnnecessaryNotNullOperator UnnecessarySafeCall UnreachableCatchBlock UnreachableCode UnsafeCallOnNullableType UnsafeCast UnusedUnaryOperator UselessPostfixExpression WrongEqualsTypeParameter
  7. Rulesets • comments • complexity • coroutines • empty-blocks •

    exceptions • naming • performance • potential-bugs • style ClassOrdering CollapsibleIfStatements DataClassContainsFunctions DataClassShouldBeImmutable DestructuringDeclarationWithTooManyEntries EqualsNullCall EqualsOnSignatureLine ExplicitCollectionElementAccessMethod ExplicitItLambdaParameter ExpressionBodySyntax ForbiddenComment ForbiddenImport ForbiddenMethodCall ForbiddenPublicDataClass ForbiddenVoid FunctionOnlyReturningConstant LibraryCodeMustSpecifyReturnType LibraryEntitiesShouldNotBePublic LoopWithTooManyJumpStatements MagicNumber MandatoryBracesIfStatements MandatoryBracesLoops MaxLineLength MayBeConst ModifierOrder MultilineLambdaItParameter NestedClassesVisibility NewLineAtEndOfFile NoTabs ObjectLiteralToLambda OptionalAbstractKeyword OptionalUnit OptionalWhenBraces PreferToOverPairSyntax ProtectedMemberInFinalClass RedundantExplicitType RedundantHigherOrderMapUsage RedundantVisibilityModifierRule ReturnCount SafeCast SerialVersionUIDInSerializableClass SpacingBetweenPackageAndImports ThrowsCount TrailingWhitespace UnderscoresInNumericLiterals UnnecessaryAbstractClass UnnecessaryAnnotationUseSiteTarget UnnecessaryApply UnnecessaryFilter UnnecessaryInheritance UnnecessaryLet UnnecessaryParentheses UntilInsteadOfRangeTo UnusedImports UnusedPrivateClass UnusedPrivateMember UseArrayLiteralsInAnnotations UseCheckNotNull UseCheckOrError UseDataClass UseEmptyCounterpart UseIfEmptyOrIfBlank UseIfInsteadOfWhen UseIsNullOrEmpty UseOrEmpty UseRequire UseRequireNotNull UselessCallOnNotNull UtilityClassWithPublicConstructor VarCouldBeVal WildcardImport
  8. UnusedPrivateMember class Sample { private val aMember = "^^" private

    val anotherMember = "U.U" fun main() { println(aMember) } } Private property `anotherMember` is unused.
  9. MagicNumber fun main() { if (Random.nextInt() > 42) { println("🐋")

    } } This expression contains a magic number. Consider defining it to a well named constant.
  10. Documentation /** * This rule detects and reports usages of

    magic numbers in the code. * Prefer defining constants with clear names * describing what the magic number means. *
  11. Documentation /** * This rule detects and reports usages of

    magic numbers in the code. * Prefer defining constants with clear names * describing what the magic number means. * * <noncompliant> * class User { * * fun checkName(name: String) { * if (name.length > 42) { * throw IllegalArgumentException("username is too long") * } * // ... * } * } * </noncompliant>
  12. Documentation /** * This rule detects and reports usages of

    magic numbers in the code. * Prefer defining constants with clear names * describing what the magic number means. * * <noncompliant> * class User { * * fun checkName(name: String) { * if (name.length > 42) { * throw IllegalArgumentException("username is too long") * } * // ... * } * } * </noncompliant>
  13. * if (name.length > 42) { * throw IllegalArgumentException("username is

    too long") * } * // ... * } * } * </noncompliant> * * <compliant> * * class User { * * fun checkName(name: String) { * if (name.length > MAX_USERNAME_SIZE) { * throw IllegalArgumentException("username is too long") * } * // ... * } * * companion object { * private const val MAX_USERNAME_SIZE = 42 * } * } * </compliant> */
  14. * } * } * </noncompliant> * * <compliant> *

    * class User { * * fun checkName(name: String) { * if (name.length > MAX_USERNAME_SIZE) { * throw IllegalArgumentException("username is too long") * } * // ... * } * * companion object { * private const val MAX_USERNAME_SIZE = 42 * } * } * </compliant> */ @Suppress("TooManyFunctions") @ActiveByDefault(since = "1.0.0") class MagicNumber(config: Config = Config.empty) : Rule(config) {
  15. * } * } * </noncompliant> * * <compliant> *

    * class User { * * fun checkName(name: String) { * if (name.length > MAX_USERNAME_SIZE) { * throw IllegalArgumentException("username is too long") * } * // ... * } * * companion object { * private const val MAX_USERNAME_SIZE = 42 * } * } * </compliant> */ @Suppress("TooManyFunctions") @ActiveByDefault(since = "1.0.0") class MagicNumber(config: Config = Config.empty) : Rule(config) {
  16. build: maxIssues: 0 excludeCorrectable: false weights: # complexity: 2 #

    LongParameterList: 1 # style: 1 # comments: 1 config: validation: true warningsAsErrors: false # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,."*>."*>[my_property]' excludes: '' processors: active: true exclude: - 'DetektProgressListener' # - 'KtFileCountProcessor' # - 'PackageCountProcessor' # - 'ClassCountProcessor' # - 'FunctionCountProcessor' # - 'PropertyCountProcessor' detekt.yml
  17. excludes: ['"**'] LibraryEntitiesShouldNotBePublic: active: true excludes: ['"**'] LoopWithTooManyJumpStatements: active: true

    maxJumpCount: 1 MagicNumber: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/ iosTest/**'] ignoreNumbers: ['-1', '0', '1', '2'] ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreLocalVariableDeclaration: false ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true ignoreAnnotation: false ignoreNamedArgument: true ignoreEnums: false ignoreRanges: false ignoreExtensionFunctions: true MandatoryBracesIfStatements: active: false MandatoryBracesLoops: active: false detekt.yml
  18. excludes: ['"**'] LibraryEntitiesShouldNotBePublic: active: true excludes: ['"**'] LoopWithTooManyJumpStatements: active: true

    maxJumpCount: 1 MagicNumber: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/ iosTest/**'] ignoreNumbers: ['-1', '0', '1', '2'] ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreLocalVariableDeclaration: false ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true ignoreAnnotation: false ignoreNamedArgument: true ignoreEnums: false ignoreRanges: false ignoreExtensionFunctions: true MandatoryBracesIfStatements: active: false MandatoryBracesLoops: active: false detekt.yml
  19. DoubleMutabilityForCollection fun main() { var hobbits = mutableListOf("Frodo", "Sam", "Merry",

    "Pippin") } Variable `hobbits` is declared as `var` with a mutable type `kotlin.collections.MutableList`. Consider using `val` or an immutable collection type
  20. ForbiddenMethodCall class Sample { private val aMember = "^^" private

    val anotherMember = "U.U" } style: ForbiddenMethodCall: active: true methods: ['kotlin.io.println', 'kotlin.io.print']
  21. ForbiddenMethodCall class Sample { private val aMember = "^^" private

    val anotherMember = "U.U" fun main() { println(aMember) } } style: ForbiddenMethodCall: active: true methods: ['kotlin.io.println', 'kotlin.io.print'] The method kotlin.io.println(String) has been forbidden in the Detekt config.
  22. • Ktlint Wrapper • Offered as Custom rules (formatting) •

    Only rules offering autoCorrection formatting: android: false autoCorrect: true AnnotationOnSeparateLine: AnnotationSpacing: ArgumentListWrapping: ChainWrapping: CommentSpacing: EnumEntryNameCase: Filename: FinalNewline: ImportOrdering: Indentation: MaximumLineLength: ModifierOrdering: MultiLineIfElse: NoBlankLineBeforeRbrace: NoConsecutiveBlankLines: NoEmptyClassBody: NoEmptyFirstLineInMethodBlock: NoLineBreakAfterElse: NoLineBreakBeforeAssignment: NoMultipleSpaces: NoSemicolons: NoTrailingSpaces: NoUnitReturn: NoUnusedImports: NoWildcardImports: PackageName: ParameterListWrapping: SpacingAroundAngleBrackets: SpacingAroundColon: SpacingAroundComma: SpacingAroundCurly: SpacingAroundDot: SpacingAroundDoubleColon: SpacingAroundKeyword: SpacingAroundOperators: SpacingAroundParens: SpacingAroundRangeOperator: SpacingAroundUnaryOperator: SpacingBetweenDeclarationsWithAnnotations: SpacingBetweenDeclarationsWithComments: StringTemplate:
  23. • Ktlint Wrapper • Offered as Custom rules (formatting) •

    Only rules offering autoCorrection formatting: android: false autoCorrect: true AnnotationOnSeparateLine: AnnotationSpacing: ArgumentListWrapping: ChainWrapping: CommentSpacing: EnumEntryNameCase: Filename: FinalNewline: ImportOrdering:
  24. formatting: active: true NoWildcardImports: active: true NoUnusedImports: active: true style:

    WildcardImport: active: true excludeImports: [‘java.util.*'] UnusedImports: active: false
  25. formatting: active: true NoWildcardImports: active: true NoUnusedImports: active: true style:

    WildcardImport: active: true excludeImports: [‘java.util.*'] UnusedImports: active: false
  26. formatting: active: true NoWildcardImports: active: true NoUnusedImports: active: true autoCorrect:

    true style: WildcardImport: active: true excludeImports: [‘java.util.*'] UnusedImports: active: false
  27. Custom Rules class MyCustomRuleSetProvider : RuleSetProvider { override val ruleSetId

    = "my-rules" override fun instance(config: Config) = RuleSet( ruleSetId, listOf(MyCustomRule(config)) ) } com.example.MyCustomRuleSetProvider class MyCustomRule( config: Config = Config.empty ) : Rule(config) { override val issue = Issue( "//… ) }
  28. Type Resolution fun main() { if (Random.nextInt() > 42) {

    println("🐋") } } val zero = 0 val answer = zero""!!.plus(42) UnnecessaryNotNullOperator MagicNumber
  29. Type Resolution fun main() { if (Random.nextInt() > 42) {

    println("🐋") } } val zero = getANumber() val answer = zero""!!.plus(42) UnnecessaryNotNullOperator MagicNumber
  30. Type Resolution fun main() { if (Random.nextInt() > 42) {

    println("🐋") } } val zero = getANumber() val answer = zero""!!.plus(42) UnnecessaryNotNullOperator MagicNumber : Int? : Int UnnecessaryNotNullOperator needs to know the return type of getANumber()
  31. Rules w/ Type Resolution /** * Reports unnecessary not-null operator

    usage ("!!) that can be removed by the user. * * <noncompliant> * val a = 1 * val b = a"!! * "</noncompliant> * * <compliant> * val a = 1 * val b = a * "</compliant> "*/ @RequiresTypeResolution @ActiveByDefault(since = "1.16.0") class UnnecessaryNotNullOperator(config: Config = Config.empty) : Rule(config) {
  32. Rules w/ Type Resolution @ActiveByDefault(since = "1.16.0") class UnnecessaryNotNullOperator(config: Config

    = Config.empty) : Rule(config) { override val issue: Issue = Issue( "UnnecessaryNotNullOperator", Severity.Defect, "Unnecessary not-null unary operator ("!!) detected.", Debt.FIVE_MINS ) override fun visitUnaryExpression(expression: KtUnaryExpression) { super.visitUnaryExpression(expression) if (bindingContext "== BindingContext.EMPTY) return "// Type Resolution is enabled here } }
  33. Rules w/ Type Resolution @ActiveByDefault(since = "1.16.0") class UnnecessaryNotNullOperator(config: Config

    = Config.empty) : Rule(config) { override val issue: Issue = Issue( "UnnecessaryNotNullOperator", Severity.Defect, "Unnecessary not-null unary operator ("!!) detected.", Debt.FIVE_MINS ) override fun visitUnaryExpression(expression: KtUnaryExpression) { super.visitUnaryExpression(expression) if (bindingContext "== BindingContext.EMPTY) return "// Type Resolution is enabled here } }
  34. plugins { id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() }

    detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt detekt detektBaseline detektGenerateConfig
  35. plugins { id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() }

    detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt detekt detektBaseline detektGenerateConfig
  36. plugins { id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() }

    detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt detekt detektBaseline detektGenerateConfig
  37. plugins { id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() }

    detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt detekt detektBaseline detektGenerateConfig
  38. $ ./gradlew tasks | grep detekt detekt detektBaseline detektBaselineMain detektBaselineTest

    detektGenerateConfig detektMain detektTest plugins { kotlin("jvm") id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) }
  39. $ ./gradlew tasks | grep detekt detekt detektBaseline detektBaselineMain detektBaselineTest

    detektGenerateConfig detektMain detektTest plugins { kotlin("jvm") id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) }
  40. $ ./gradlew tasks | grep detekt detekt detektBaseline detektBaselineMain detektBaselineTest

    detektGenerateConfig detektMain detektTest plugins { kotlin("jvm") id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories { mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) }
  41. plugins { kotlin(“android") id(“com.android.application”) id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories {

    mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt app:detekt app:detektBaseline app:detektBaselineDebug app:detektBaselineDebugAndroidTest app:detektBaselineDebugUnitTest app:detektBaselineMain app:detektBaselineRelease app:detektBaselineReleaseUnitTest app:detektBaselineTest app:detektDebug app:detektDebugAndroidTest app:detektDebugUnitTest app:detektGenerateConfig app:detektMain app:detektRelease app:detektReleaseUnitTest app:detektTest
  42. plugins { kotlin(“android") id(“com.android.application”) id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories {

    mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt app:detekt app:detektBaseline app:detektBaselineDebug app:detektBaselineDebugAndroidTest app:detektBaselineDebugUnitTest app:detektBaselineMain app:detektBaselineRelease app:detektBaselineReleaseUnitTest app:detektBaselineTest app:detektDebug app:detektDebugAndroidTest app:detektDebugUnitTest app:detektGenerateConfig app:detektMain app:detektRelease app:detektReleaseUnitTest app:detektTest
  43. plugins { kotlin(“android") id(“com.android.application”) id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories {

    mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt app:detekt app:detektBaseline app:detektBaselineDebug app:detektBaselineDebugAndroidTest app:detektBaselineDebugUnitTest app:detektBaselineMain app:detektBaselineRelease app:detektBaselineReleaseUnitTest app:detektBaselineTest app:detektDebug app:detektDebugAndroidTest app:detektDebugUnitTest app:detektGenerateConfig app:detektMain app:detektRelease app:detektReleaseUnitTest app:detektTest
  44. plugins { kotlin(“android") id(“com.android.application”) id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories {

    mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) } $ ./gradlew tasks | grep detekt app:detekt app:detektBaseline app:detektBaselineDebug app:detektBaselineDebugAndroidTest app:detektBaselineDebugUnitTest app:detektBaselineMain app:detektBaselineRelease app:detektBaselineReleaseUnitTest app:detektBaselineTest app:detektDebug app:detektDebugAndroidTest app:detektDebugUnitTest app:detektGenerateConfig app:detektMain app:detektRelease app:detektReleaseUnitTest app:detektTest
  45. Multiplatform plugins { kotlin(“multiplatform") id("io.gitlab.arturbosch.detekt") version "1.17.1" } repositories {

    mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) } kotlin { jvm() android() ios() js() }
  46. Multiplatform $ ./gradlew tasks | grep detekt detekt detektAndroidDebug detektAndroidDebugAndroidTest

    detektAndroidDebugUnitTest detektAndroidRelease detektAndroidReleaseUnitTest detektBaseline detektBaselineAndroidDebug detektBaselineAndroidDebugAndroidTest detektBaselineAndroidDebugUnitTest detektBaselineAndroidRelease detektBaselineAndroidReleaseUnitTest detektBaselineIosArm64Main detektBaselineIosArm64Test detektBaselineIosX64Main detektBaselineIosX64Test detektBaselineJsMain detektBaselineJsTest detektBaselineJvmMain detektBaselineJvmTest detektBaselineMetadataCommonMain detektBaselineMetadataIosMain detektBaselineMetadataMain detektGenerateConfig detektIosArm64Main detektIosArm64Test detektIosX64Main detektIosX64Test detektJsMain detektJsTest detektJvmMain detektJvmTest detektMetadataCommonMain detektMetadataIosMain detektMetadataMain
  47. detektMain (w/ Type Resolution) compileKotlin Runs another compilation Internally to

    resolve types ./gradlew check • Running two compilations is bad for performances • Misaligned compiler flags • Harder to get the correct class path every time (ViewBinding, etc.)
  48. detekt-compiler-plugin plugins { kotlin("jvm") id("io.gitlab.arturbosch.detekt") version “…” } repositories {

    mavenCentral() } detekt { config = files(“config/detekt/detekt.yml”) }
  49. compileKotlin Runs detekt during the compilation ./gradlew check • No

    more double compilation • Types Resolution close to the production code • No more custom tasks for every source set
  50. detekt-compiler-plugin $ ./gradlew assemble > Task :compileKotlin FAILED w: Build

    failed with 1 weighted issues. e: warnings found and -Werror specified w: /Users/ncorti/oss/detekt-compiler-plugin/src/main/java/Sample.kt: (3, 21): MagicNumber: This expression contains a magic number. Consider defining it to a well named constant. FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':compileKotlin'. > Compilation error. See log for more details BUILD FAILED in 1s 1 actionable task: 1 executed
  51. detekt-compiler-plugin $ ./gradlew assemble > Task :compileKotlin FAILED w: Build

    failed with 1 weighted issues. e: warnings found and -Werror specified w: /Users/ncorti/oss/detekt-compiler-plugin/src/main/java/Sample.kt: (3, 21): MagicNumber: This expression contains a magic number. Consider defining it to a well named constant. FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':compileKotlin'. > Compilation error. See log for more details BUILD FAILED in 1s 1 actionable task: 1 executed
  52. Where is ? • github.com/detekt/detekt • #detekt on KotlinLang •

    detekt.github.io/ • Discussions & Github Issues
  53. • Search for • Update documentation • Add more tests

    • Submit new rules ideas • Fix false positives/false negatives • Help with integrations • Test new versions (-SNAPSHOT) • Help with branding How to ?